From fd2ce60ebae6be747be106c50bbd402b5c03ff95 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 08:39:01 -0700 Subject: [PATCH 01/13] Split Docker builds: fast PR smoke tests and cron-only publishing PRs no longer build the full 36-image matrix. test-pull-request.yml now runs unit tests plus a path-gated smoke build (NxMeta + NxMeta-LSIO, amd64 only, no push) that only runs when image files change, giving fast PR feedback. Publishing moves to schedule/manual only. publish-release.yml becomes the sole publisher: it builds the base images once and then builds and pushes the full matrix for both the main and develop branches in one run, plus the GitHub release, Docker Hub readme, and date badge. The push-triggered publish (old publish-release.yml) and the separate weekly publisher (publish-periodic-docker-release.yml) are removed, and build-release-task.yml is retired. Merges to main/develop no longer build or publish images, so auto-merged Dependabot and codegen PRs stop producing no-op image updates for consumers; the next scheduled publish picks them up. build-docker-task.yml gains smoke/ref/build_base inputs and branch-scoped matrix filtering; build-base-images-task.yml and get-version-task.yml gain platforms/ref inputs. Codegen schedule changed to daily to match Dependabot. Docs (README, HISTORY, AGENTS, copilot-instructions) and version updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/copilot-instructions.md | 6 + .github/workflows/build-base-images-task.yml | 10 +- .github/workflows/build-datebadge-task.yml | 2 +- .github/workflows/build-docker-task.yml | 260 +++++++++++------- .github/workflows/build-release-task.yml | 50 ---- .github/workflows/get-version-task.yml | 6 + .github/workflows/merge-bot-pull-request.yml | 16 +- .../publish-periodic-docker-release.yml | 28 -- .github/workflows/publish-release.yml | 128 ++++++--- .../run-periodic-codegen-pull-request.yml | 4 +- .github/workflows/test-pull-request.yml | 108 +++++--- .github/workflows/test-release-task.yml | 9 - AGENTS.md | 47 ++-- HISTORY.md | 2 + README.md | 6 +- version.json | 2 +- 16 files changed, 385 insertions(+), 299 deletions(-) delete mode 100644 .github/workflows/build-release-task.yml delete mode 100644 .github/workflows/publish-periodic-docker-release.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fa9f57ca..9b20856b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,6 +38,12 @@ This repository builds and publishes Docker images for Network Optix VMS product - Base images (`nx-base`, `nx-base-lsio`) are built and pushed, then used as `FROM` images for derived product Dockerfiles. - Derived images should track base image tag changes (for example, the Ubuntu distro tag) to keep builds consistent. +### CI Pipeline (GitHub Actions) + +- Pull requests run unit tests and style checks, plus a fast smoke build (NxMeta and NxMeta-LSIO, amd64 only, no push) that runs only when image files change -- not the full matrix. +- Publishing is schedule/manual only via `publish-release.yml`, which builds the base images once and then publishes the full matrix for both the `main` and `develop` branches in a single run. +- Merges to `main`/`develop` do not publish; auto-merged Dependabot and codegen PRs are picked up by the next scheduled publish. Do not reintroduce push-triggered publishing or full-matrix PR builds. Validate workflow edits with `actionlint`. + ## What to Keep in Sync - Generated Dockerfiles and scripts must reflect CreateMatrix behavior. diff --git a/.github/workflows/build-base-images-task.yml b/.github/workflows/build-base-images-task.yml index 4e8136e2..a380395f 100644 --- a/.github/workflows/build-base-images-task.yml +++ b/.github/workflows/build-base-images-task.yml @@ -7,6 +7,10 @@ on: required: false type: boolean default: true + platforms: + required: false + type: string + default: linux/amd64,linux/arm64 jobs: @@ -36,12 +40,12 @@ jobs: - name: Setup QEMU step uses: docker/setup-qemu-action@v4 with: - platforms: linux/amd64,linux/arm64 + platforms: ${{ inputs.platforms }} - name: Setup Buildx step uses: docker/setup-buildx-action@v4 with: - platforms: linux/amd64,linux/arm64 + platforms: ${{ inputs.platforms }} - name: Login to Docker Hub step uses: docker/login-action@v4 @@ -56,7 +60,7 @@ jobs: push: ${{ inputs.push && (github.ref_name == 'main' || github.ref_name == 'develop') }} context: Docker file: ${{ matrix.base.dockerfile }} - platforms: linux/amd64,linux/arm64 + platforms: ${{ inputs.platforms }} tags: ${{ matrix.base.tags }} cache-from: | type=registry,ref=${{ matrix.base.cache_tag }} diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index 8f7f8f8d..bb0edfe8 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -14,7 +14,7 @@ jobs: - name: Get current date step id: date run: | - echo "date=$(date)" >> $GITHUB_OUTPUT + echo "date=$(date)" >> "$GITHUB_OUTPUT" - name: Build BYOB date badge step if: ${{ github.ref_name == 'main' }} diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index f5bc63b6..cacc8542 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -1,104 +1,156 @@ -name: Build Docker image task - - -on: - workflow_call: - inputs: - push: - required: false - type: boolean - default: false - -jobs: - - get-matrix: - name: Get matrix job - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.getmatrix.outputs.matrix }} - - steps: - - - name: Checkout step - uses: actions/checkout@v6 - - - name: Load matrix.json step - id: getmatrix - run: | - echo "matrix=$(jq --compact-output '.' ./Make/Matrix.json)" >> $GITHUB_OUTPUT - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-base: - name: Build base image job - uses: ./.github/workflows/build-base-images-task.yml - with: - push: ${{ inputs.push }} - secrets: inherit - - build-docker: - name: Build Docker image job - runs-on: ubuntu-latest - needs: [get-version, get-matrix, build-base] - - strategy: - max-parallel: 4 - matrix: - images: ${{ fromJson(needs.get-matrix.outputs.matrix).Images }} - - steps: - - - name: Checkout step - uses: actions/checkout@v6 - - - name: Setup QEMU step - uses: docker/setup-qemu-action@v4 - with: - platforms: linux/amd64,linux/arm64 - - - name: Setup Buildx step - uses: docker/setup-buildx-action@v4 - with: - platforms: linux/amd64,linux/arm64 - - - name: Login to Docker Hub step - uses: docker/login-action@v4 - with: - registry: docker.io - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Create tags and args step - id: tagsargs - run: | - TAGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Tags) }}') - ARGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Args) }}') - { - echo "tags<> "$GITHUB_OUTPUT" - - - name: Docker build and push step - uses: docker/build-push-action@v7 - with: - push: ${{ inputs.push && (github.ref_name == matrix.images.Branch) }} - context: Docker - file: Docker/${{ matrix.images.Name }}.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.tagsargs.outputs.tags }} - build-args: ${{ steps.tagsargs.outputs.args }} - cache-from: | - type=gha,scope=develop-${{ matrix.images.Name }} - type=gha,scope=main-${{ matrix.images.Name }} - ${{ github.event.pull_request && format('type=gha,scope=pr-{0}-{1}', github.event.pull_request.number, matrix.images.Name) || '' }} - cache-to: | - ${{ (github.ref_name == 'main' || github.ref_name == 'develop') && format('type=gha,mode=min,scope={0}-{1},ignore-error=true', github.ref_name, matrix.images.Name) || '' }} - ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork && format('type=gha,mode=min,scope=pr-{0}-{1},ignore-error=true', github.event.pull_request.number, matrix.images.Name) || '' }} +name: Build Docker image task + + +on: + workflow_call: + inputs: + push: + required: false + type: boolean + default: false + # Smoke mode: build a single Ubuntu + single LSIO variant on amd64 + # only, never push. Used for fast PR feedback in place of the full + # matrix. See test-pull-request.yml. + smoke: + required: false + type: boolean + default: false + # Branch to check out and whose images to build/push. Empty uses the + # triggering ref (PR context). The publisher passes main and develop + # so both branches' tags are produced from one scheduled run. + ref: + required: false + type: string + default: '' + # When false the caller is expected to have built the base images + # already (the publisher builds them once, see publish-release.yml), + # so the internal base build is skipped. + build_base: + required: false + type: boolean + default: true + +jobs: + + get-matrix: + name: Get matrix job + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.getmatrix.outputs.matrix }} + + steps: + + - name: Checkout step + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Load matrix.json step + id: getmatrix + env: + SMOKE: ${{ inputs.smoke }} + REF: ${{ inputs.ref }} + run: | + if [[ "$SMOKE" == "true" ]]; then + # One Ubuntu + one LSIO variant exercises the shared Dockerfile + # build logic; tags/args are irrelevant since smoke never pushes. + FILTER='.Images |= (map(select(.Name == "NxMeta" or .Name == "NxMeta-LSIO")) | unique_by(.Name))' + elif [[ -n "$REF" ]]; then + # Publish: build only the rows targeting the branch being built + # (avoids building the other branch's rows just to discard them). + FILTER=".Images |= map(select(.Branch == \"$REF\"))" + else + FILTER='.' + fi + echo "matrix=$(jq --compact-output "$FILTER" ./Make/Matrix.json)" >> "$GITHUB_OUTPUT" + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + with: + ref: ${{ inputs.ref }} + + build-base: + name: Build base image job + if: ${{ inputs.build_base }} + uses: ./.github/workflows/build-base-images-task.yml + with: + push: ${{ inputs.push }} + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + secrets: inherit + + build-docker: + name: Build Docker image job + runs-on: ubuntu-latest + needs: [get-version, get-matrix, build-base] + # always() so the job still runs when build-base is intentionally + # skipped (build_base == false); fail only if a prerequisite failed. + if: >- + ${{ always() + && needs.get-version.result == 'success' + && needs.get-matrix.result == 'success' + && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') }} + + strategy: + max-parallel: 4 + matrix: + images: ${{ fromJson(needs.get-matrix.outputs.matrix).Images }} + + steps: + + - name: Checkout step + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Setup QEMU step + uses: docker/setup-qemu-action@v4 + with: + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + - name: Setup Buildx step + uses: docker/setup-buildx-action@v4 + with: + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + - name: Login to Docker Hub step + uses: docker/login-action@v4 + with: + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Create tags and args step + id: tagsargs + run: | + TAGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Tags) }}') + ARGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Args) }}') + { + echo "tags<> "$GITHUB_OUTPUT" + + - name: Docker build and push step + uses: docker/build-push-action@v7 + with: + # Matrix is already filtered to the target branch, so push when asked. + # Smoke callers pass push:false. + push: ${{ inputs.push }} + context: Docker + file: Docker/${{ matrix.images.Name }}.Dockerfile + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + tags: ${{ steps.tagsargs.outputs.tags }} + build-args: ${{ steps.tagsargs.outputs.args }} + cache-from: | + type=gha,scope=develop-${{ matrix.images.Name }} + type=gha,scope=main-${{ matrix.images.Name }} + ${{ github.event.pull_request && format('type=gha,scope=pr-{0}-{1}', github.event.pull_request.number, matrix.images.Name) || '' }} + cache-to: | + ${{ inputs.push && format('type=gha,mode=min,scope={0}-{1},ignore-error=true', inputs.ref != '' && inputs.ref || github.ref_name, matrix.images.Name) || '' }} + ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork && format('type=gha,mode=min,scope=pr-{0}-{1},ignore-error=true', github.event.pull_request.number, matrix.images.Name) || '' }} diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml deleted file mode 100644 index e940a2f0..00000000 --- a/.github/workflows/build-release-task.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build project release task - -on: - workflow_call: - inputs: - github: - required: false - type: boolean - default: false - dockerhub: - required: false - type: boolean - default: false - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-docker: - name: Build Docker job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - push: ${{ inputs.dockerhub }} - - github-release: - name: Publish GitHub release job - if: ${{ inputs.github }} - runs-on: ubuntu-latest - needs: [get-version, build-docker] - - steps: - - - name: Checkout code step - uses: actions/checkout@v6 - - - name: Create GitHub release job - uses: softprops/action-gh-release@v3 - with: - generate_release_notes: true - tag_name: ${{ needs.get-version.outputs.SemVer2 }} - prerelease: ${{ github.ref_name != 'main' }} - files: | - LICENSE - README.md - ./Make/Version.json - ./Make/Matrix.json diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index d88cb354..f8890ed7 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -2,6 +2,11 @@ name: Get version information task on: workflow_call: + inputs: + ref: + required: false + type: string + default: '' outputs: SemVer2: value: ${{ jobs.get-version.outputs.SemVer2 }} @@ -34,6 +39,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + ref: ${{ inputs.ref }} - name: Run Nerdbank.GitVersioning tool step id: nbgv diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index dfeb9dc2..3dc8b0c7 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -22,13 +22,15 @@ name: Merge bot pull request action # # Token strategy: # Every job uses an App token (`actions/create-github-app-token`). -# The resulting push is committed by the App, which fires downstream -# workflows on develop and main. `GITHUB_TOKEN`-authored pushes are -# blocked from triggering further workflow runs by GitHub's recursion -# guard, which would silently skip `publish-release.yml` on the merge -# commit. The App-token path also removes the close/reopen dance -# previously used by codegen PRs created under `GITHUB_TOKEN` to nudge -# the auto-merge workflow. The disable job needs an App token too: +# Bot PRs are opened under the App token so the `pull_request` event +# fires the auto-merge and PR-test workflows directly; `GITHUB_TOKEN`- +# authored PR opens are blocked from triggering further workflow runs +# by GitHub's recursion guard. The App-token path also removes the +# close/reopen dance previously used by codegen PRs created under +# `GITHUB_TOKEN` to nudge the auto-merge workflow. (Merges no longer +# trigger publishing at all -- the scheduled `publish-release.yml` is +# the sole publisher -- so no merge-commit workflow needs to fire.) +# The disable job needs an App token too: # even though the event actor is a maintainer, the workflow context # on a Dependabot PR runs with Dependabot's restricted secrets # regardless of actor, so plain `GITHUB_TOKEN` would be read-only. diff --git a/.github/workflows/publish-periodic-docker-release.yml b/.github/workflows/publish-periodic-docker-release.yml deleted file mode 100644 index e37eae31..00000000 --- a/.github/workflows/publish-periodic-docker-release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Publish weekly Docker image to Docker Hub action - -on: - workflow_dispatch: - schedule: - - cron: '0 2 * * MON' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - build-docker: - name: Build Docker image job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Push to registry - push: true - - date-badge: - name: Create BYOB date badge job - needs: [build-docker] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 25bc2d93..7d9decfe 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,36 +1,92 @@ -name: Publish project release action - -on: - push: - branches: [ main, develop ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - create-release: - name: Publish project release job - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - permissions: - contents: write - with: - github: true - dockerhub: true - - docker-readme: - name: Publish docker hub readme job - needs: [create-release] - uses: ./.github/workflows/publish-docker-readme-task.yml - secrets: inherit - - date-badge: - name: Create BYOB date badge job - needs: [create-release] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write +name: Publish project release action + +# Sole publisher. Images are built and pushed ONLY here: on the weekly +# schedule or via manual dispatch. Merges to main/develop no longer build +# or publish (PRs get a fast smoke build instead, see test-pull-request.yml). +# One run publishes both the main (:latest/:stable) and develop (:develop) +# tags, picking up everything merged since the last run (codegen version +# bumps, Dependabot base-image bumps, code changes). + +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * MON' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + # Base images share a single tag (nx-base:ubuntu-noble) across branches, + # so build them once (from the release branch) and let both branch legs + # reuse them via build_base: false. + build-base: + name: Build base image job + uses: ./.github/workflows/build-base-images-task.yml + secrets: inherit + with: + push: true + + build-main: + name: Build main images job + needs: [build-base] + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + push: true + ref: main + build_base: false + + build-develop: + name: Build develop images job + needs: [build-base] + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + push: true + ref: develop + build_base: false + + github-release: + name: Publish GitHub release job + needs: [get-version, build-main] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + + - name: Checkout code step + uses: actions/checkout@v6 + + - name: Create GitHub release step + uses: softprops/action-gh-release@v3 + with: + generate_release_notes: true + tag_name: ${{ needs.get-version.outputs.SemVer2 }} + prerelease: ${{ github.ref_name != 'main' }} + files: | + LICENSE + README.md + ./Make/Version.json + ./Make/Matrix.json + + docker-readme: + name: Publish docker hub readme job + needs: [build-main, build-develop] + uses: ./.github/workflows/publish-docker-readme-task.yml + secrets: inherit + + date-badge: + name: Create BYOB date badge job + needs: [build-main, build-develop] + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write diff --git a/.github/workflows/run-periodic-codegen-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index 486214b2..f1827457 100644 --- a/.github/workflows/run-periodic-codegen-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-pull-request.yml @@ -1,9 +1,9 @@ -name: Run weekly CodeGen and Pull Request action +name: Run daily CodeGen and Pull Request action on: workflow_dispatch: schedule: - - cron: '0 2 * * SUN' + - cron: '0 2 * * *' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 05f4fa22..87a4855f 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,36 +1,72 @@ -name: Test pull request action - -on: - pull_request: - branches: [ main, develop ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - test-release: - name: Test release job - uses: ./.github/workflows/test-release-task.yml - secrets: inherit - - # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs - # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 - check-workflow-status: - name: Check pull request workflow status - runs-on: ubuntu-latest - needs: - [ test-release ] - if: always() - steps: - - name: Check workflow results - run: | - exit_on_result() { - if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then - echo "Job '$1' failed or was cancelled." - exit 1 - fi - } - exit_on_result "test-release" "${{ needs.test-release.result }}" +name: Test pull request action + +on: + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + changes: + name: Detect image changes job + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + image: ${{ steps.filter.outputs.image }} + steps: + - name: Filter changed paths step + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + image: + - 'Docker/**' + - 'Make/Matrix.json' + - 'Make/Version.json' + + test-release: + name: Test release job + uses: ./.github/workflows/test-release-task.yml + secrets: inherit + + # Fast smoke build in place of the full matrix: only runs when image + # files changed, and builds just NxMeta + NxMeta-LSIO on amd64 (no push). + # See build-docker-task.yml `smoke`. Publishing happens only on the + # scheduled publish-release.yml, so a smoke pass is fast PR feedback, + # not a publish gate. + smoke-build: + name: Smoke build job + needs: [changes] + if: ${{ needs.changes.outputs.image == 'true' }} + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + push: false + smoke: true + + # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs + # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 + check-workflow-status: + name: Check pull request workflow status + runs-on: ubuntu-latest + needs: + [ test-release, smoke-build ] + if: always() + steps: + - name: Check workflow results + run: | + exit_on_result() { + # Pass on success or skipped (smoke-build is skipped when no + # image files changed); fail only on failure/cancelled. + if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then + echo "Job '$1' failed or was cancelled." + exit 1 + fi + } + exit_on_result "test-release" "${{ needs.test-release.result }}" + exit_on_result "smoke-build" "${{ needs.smoke-build.result }}" diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml index 0c808df5..dfa52786 100644 --- a/.github/workflows/test-release-task.yml +++ b/.github/workflows/test-release-task.yml @@ -28,12 +28,3 @@ jobs: - name: Run unit tests step run: dotnet test - - build-release: - name: Build release without publishing job - needs: [unit-test] - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - with: - github: false - dockerhub: false diff --git a/AGENTS.md b/AGENTS.md index 6cda8d85..4deb03fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Instructions for AI Coding Agents -This repository builds and publishes Docker images for Network Optix VMS products (Nx Witness, Nx Meta, Nx Go, DW Spectrum, Wisenet WAVE). It includes base images (nx-base, nx-base-lsio) and derived product images that use the base images, plus a .NET tooling project that generates Dockerfiles and build matrices and scripts/templates for packaging. +This repository builds and publishes Docker images for Network Optix VMS products (Nx Witness, Nx Meta, Nx Go, DW Spectrum, Wisenet WAVE). It includes base images (nx-base, nx-base-lsio) and derived product images that use the base images, plus a .NET tooling project that generates Dockerfiles and build matrices and scripts/templates for packaging. For comprehensive coding and formatting standards, follow: @@ -19,10 +19,10 @@ For comprehensive coding and formatting standards, follow: - `CreateMatrixTests/CreateMatrixTests.csproj` - xUnit v3 test project with AwesomeAssertions. -### Key Directories - -- `Docker/` - - Generated and static Dockerfiles for base images and product variants. +### Key Directories + +- `Docker/` + - Generated and static Dockerfiles for base images and product variants. - `Make/` - Build orchestration scripts and test compose files. - `Unraid/` @@ -30,21 +30,28 @@ For comprehensive coding and formatting standards, follow: - `version.json`, `Make/Version.json`, `Make/Matrix.json` - Version and matrix inputs consumed by the build pipeline. -## Build and Validation Workflow +## Build and Validation Workflow - Primary developer entry points are the `CreateMatrix` CLI commands invoked directly or via the scripts in `Make/`: - `version --versionpath=./Make/Version.json`. - `matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion`. - `make --versionpath=./Make/Version.json --makedirectory=./Make --dockerdirectory=./Docker --versionlabel=Beta`. -- Formatting and style checks are enforced by Husky.Net and VS Code tasks. -- Required tasks are documented in `CODESTYLE.md` and `.husky/task-runner.json`. -- C# code should be formatted with CSharpier, then verified with `dotnet format` (style). -- The `.Net Format` VS Code task in `.vscode/tasks.json` must be clean and warning-free at all times. - -## Image Architecture - -- Base images (`nx-base`, `nx-base-lsio`) are built and pushed, then reused as `FROM` images for derived product Dockerfiles. -- Derived product images should stay aligned with the base image changes and tags (for example, the Ubuntu distro tag). +- Formatting and style checks are enforced by Husky.Net and VS Code tasks. +- Required tasks are documented in `CODESTYLE.md` and `.husky/task-runner.json`. +- C# code should be formatted with CSharpier, then verified with `dotnet format` (style). +- The `.Net Format` VS Code task in `.vscode/tasks.json` must be clean and warning-free at all times. + +## Image Architecture + +- Base images (`nx-base`, `nx-base-lsio`) are built and pushed, then reused as `FROM` images for derived product Dockerfiles. +- Derived product images should stay aligned with the base image changes and tags (for example, the Ubuntu distro tag). + +## CI Pipeline (GitHub Actions) + +- Pull requests (`test-pull-request.yml`) run unit tests and code style, plus a fast smoke build only when image files (`Docker/**`, `Make/Matrix.json`, `Make/Version.json`) change. The smoke build (`build-docker-task.yml` with `smoke: true`) builds a representative subset (NxMeta and NxMeta-LSIO, amd64 only, no push), not the full matrix. +- Publishing happens only on a schedule or manual dispatch (`publish-release.yml`): it builds the base images once, then builds and pushes the full matrix for both the `main` and `develop` branches (`build-docker-task.yml` with `ref:` and `build_base: false`), and updates the GitHub release, Docker Hub readme, and date badge. +- Merges to `main`/`develop` do not build or publish images. Auto-merged Dependabot and codegen PRs simply land commits that the next scheduled publish picks up. Do not reintroduce push-triggered publishing or full-matrix PR builds. +- Validate workflow edits with `actionlint` before pushing. ## Coding Conventions (Highlights) @@ -54,8 +61,8 @@ For comprehensive coding and formatting standards, follow: - UTF-8 encoding without BOM. - Respect line ending rules in `.editorconfig`. -## Notes for Changes - -- When modifying Dockerfiles or build scripts, ensure generated outputs stay in sync with `CreateMatrix` behavior. -- Keep base image definitions and derived image Dockerfiles aligned, since derived images build on the base images. -- Keep `README.md` and release documentation aligned with build outputs and product variants. +## Notes for Changes + +- When modifying Dockerfiles or build scripts, ensure generated outputs stay in sync with `CreateMatrix` behavior. +- Keep base image definitions and derived image Dockerfiles aligned, since derived images build on the base images. +- Keep `README.md` and release documentation aligned with build outputs and product variants. diff --git a/HISTORY.md b/HISTORY.md index 424986a3..5e58dd4d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,8 @@ This is a project to build and publish docker images for various [Network Optix] ## Release History +- Version 2.12: + - Reworked the CI pipeline: pull requests run a fast representative amd64 smoke build (NxMeta and NxMeta-LSIO) instead of the full matrix, publishing moved to a weekly schedule (and manual trigger) that builds both the `main` and `develop` branches in one run, and merges no longer republish images. This speeds up PR feedback, reduces GH Actions usage, and stops no-op image updates for consumers. - Version 2.11: - Add `currentOsVariantOverride=docker` to `mediaserver.conf` following the pattern used in the current Nx [Dockerfile][nxvmsdockerfile-link], and documented in this [support][nxpackage-link] article. - Version 2.10: diff --git a/README.md b/README.md index 88f7f0de..6ec67171 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Notes: - `latest` and `stable` may be the same version if all builds are released builds. - `rc` and `beta` tags are only built when RC and Beta builds are published by Nx, and may be older than current `latest` or `stable` builds. -- Images are updated weekly, picking up the latest upstream Ubuntu updates and newly released Nx product versions. +- Images are published once a week on a schedule (and on-demand via manual trigger), picking up the latest upstream Ubuntu updates and newly released Nx product versions. A single scheduled run publishes both the `main` tags (`latest`, `stable`, version numbers) and the `develop` tags. Merging code or dependency updates does not republish images, so the published images only change when there is an actual content change. - See [Build Process](#build-process) for more details. **Docker releases**: @@ -452,7 +452,9 @@ services: - The logic follows the same pattern as used by the [Nx Open][releaseinfo-link] desktop client logic. - The "released" status of a build follows the same method as Nx uses in [`isBuildPublished()`][isbuildpublished-link] where `release_date` and `release_delivery_days` from the [Releases JSON API][nxwitnessreleases-link] must be greater than `0` - [`Matrix.json`](./Make/Matrix.json) is created from the `Version.json` file and is used during pipeline builds using a [Matrix][matrix-link] strategy. -- Automated builds are done using [GitHub Actions](https://docs.github.com/en/actions) and the [`BuildPublishPipeline.yml`](./.github/workflows/BuildPublishPipeline.yml) pipeline. +- Automated builds use [GitHub Actions](https://docs.github.com/en/actions): + - Pull requests run unit tests, and when image files change, a fast representative amd64 smoke build of `NxMeta` and `NxMeta-LSIO` ([`test-pull-request.yml`](./.github/workflows/test-pull-request.yml)) -- the full matrix is not built on every PR. + - Publishing happens only on a weekly schedule or manual trigger ([`publish-release.yml`](./.github/workflows/publish-release.yml)), which builds and pushes the full matrix for both the `main` and `develop` branches. Merges to `main`/`develop` (including auto-merged Dependabot and codegen updates) do not publish; the next scheduled run picks them up. - Version history is maintained and used by `CreateMatrix` such that generic tags, e.g. `latest`, will never result in a lesser version number, i.e. break-fix-forward only, see [Issue #62](https://github.com/ptr727/NxWitness/issues/62) for details on Nx re-publishing "released" builds using an older version breaking already upgraded systems. **Local testing**: diff --git a/version.json b/version.json index f42f73fb..2f7892be 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.11", + "version": "2.12", "publicReleaseRefSpec": [ "^refs/heads/main$" ], From 1631ca54a309eb9c586c0a8b0addafeea965c415 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 08:40:39 -0700 Subject: [PATCH 02/13] Document PR Copilot review process in AGENTS.md Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4deb03fb..4a56ce66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,12 @@ For comprehensive coding and formatting standards, follow: - Merges to `main`/`develop` do not build or publish images. Auto-merged Dependabot and codegen PRs simply land commits that the next scheduled publish picks up. Do not reintroduce push-triggered publishing or full-matrix PR builds. - Validate workflow edits with `actionlint` before pushing. +## Pull Request Review Process + +- Open PRs against `develop` (the integration branch); `develop` is forward-only and ships to `main` via release merges. +- The repo is configured to automatically request a GitHub Copilot review when a PR is opened. Respond to every Copilot comment: either address it with a change, or justify why it does not apply. Either way, reply on the comment stating what you did, then resolve (close) the comment. +- After you push a new commit, the automatic Copilot re-review is flaky and often does not fire. If you cannot trigger a re-review, ask the maintainer to start one in the GitHub UI. Repeat until both Copilot and the author are satisfied. + ## Coding Conventions (Highlights) - Do not use `var`; use explicit types. From 13aee88c6358dbfd8444803c71152789b650d626 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 08:48:53 -0700 Subject: [PATCH 03/13] Address Copilot review: ref fallback, gated login, safe jq, concurrency - Checkout steps fall back to github.ref when inputs.ref is empty (get-version-task, build-docker-task x2, build-base-images-task). - Gate Docker Hub login on inputs.push so non-publishing/smoke builds (and forked PRs without secrets) don't require credentials (build-docker-task, build-base-images-task). - Pass the branch ref to jq via --arg instead of string interpolation (build-docker-task), with a shellcheck SC2016 directive documenting that $ref is a jq variable. - Add a ref input to build-base-images-task and call it with ref: main from publish-release so a develop dispatch can't overwrite the shared nx-base tag. - Use a single global concurrency group for publish-release so dispatches on different refs can't race on shared tags/readme/badge. - changes job: add contents: read + checkout so paths-filter works on non-PR triggers (workflow_dispatch). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-base-images-task.yml | 13 +++++++++++++ .github/workflows/build-docker-task.yml | 18 ++++++++++++++---- .github/workflows/get-version-task.yml | 2 +- .github/workflows/publish-release.yml | 8 +++++++- .github/workflows/test-pull-request.yml | 6 ++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-base-images-task.yml b/.github/workflows/build-base-images-task.yml index a380395f..b197dfd2 100644 --- a/.github/workflows/build-base-images-task.yml +++ b/.github/workflows/build-base-images-task.yml @@ -11,6 +11,13 @@ on: required: false type: string default: linux/amd64,linux/arm64 + # Branch to check out. The publisher passes main so the shared + # nx-base tag is always built from the release branch regardless of + # the dispatch ref. Empty falls back to the triggering ref. + ref: + required: false + type: string + default: '' jobs: @@ -36,6 +43,8 @@ jobs: - name: Checkout step uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref }} - name: Setup QEMU step uses: docker/setup-qemu-action@v4 @@ -47,7 +56,11 @@ jobs: with: platforms: ${{ inputs.platforms }} + # Only needed when pushing; skipped for non-publishing builds (e.g. + # PR smoke) so they don't depend on Docker Hub secrets (unavailable + # on forked PRs). Public base images still pull anonymously. - name: Login to Docker Hub step + if: ${{ inputs.push }} uses: docker/login-action@v4 with: registry: docker.io diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index cacc8542..070d54ed 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -43,7 +43,7 @@ jobs: - name: Checkout step uses: actions/checkout@v6 with: - ref: ${{ inputs.ref }} + ref: ${{ inputs.ref || github.ref }} - name: Load matrix.json step id: getmatrix @@ -51,6 +51,9 @@ jobs: SMOKE: ${{ inputs.smoke }} REF: ${{ inputs.ref }} run: | + # $ref below is a jq variable (passed via --arg), not a shell + # variable, so it must stay single-quoted / unexpanded. + # shellcheck disable=SC2016 if [[ "$SMOKE" == "true" ]]; then # One Ubuntu + one LSIO variant exercises the shared Dockerfile # build logic; tags/args are irrelevant since smoke never pushes. @@ -58,11 +61,13 @@ jobs: elif [[ -n "$REF" ]]; then # Publish: build only the rows targeting the branch being built # (avoids building the other branch's rows just to discard them). - FILTER=".Images |= map(select(.Branch == \"$REF\"))" + # $ref is passed via --arg so the value is never interpolated + # into the jq program. + FILTER='.Images |= map(select(.Branch == $ref))' else FILTER='.' fi - echo "matrix=$(jq --compact-output "$FILTER" ./Make/Matrix.json)" >> "$GITHUB_OUTPUT" + echo "matrix=$(jq --arg ref "$REF" --compact-output "$FILTER" ./Make/Matrix.json)" >> "$GITHUB_OUTPUT" get-version: name: Get version information job @@ -78,6 +83,7 @@ jobs: with: push: ${{ inputs.push }} platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + ref: ${{ inputs.ref }} secrets: inherit build-docker: @@ -102,7 +108,7 @@ jobs: - name: Checkout step uses: actions/checkout@v6 with: - ref: ${{ inputs.ref }} + ref: ${{ inputs.ref || github.ref }} - name: Setup QEMU step uses: docker/setup-qemu-action@v4 @@ -114,7 +120,11 @@ jobs: with: platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + # Only needed when pushing; skipped for non-publishing builds (e.g. + # PR smoke) so they don't depend on Docker Hub secrets (unavailable + # on forked PRs). The public base image still pulls anonymously. - name: Login to Docker Hub step + if: ${{ inputs.push }} uses: docker/login-action@v4 with: registry: docker.io diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index f8890ed7..ed9b7898 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - ref: ${{ inputs.ref }} + ref: ${{ inputs.ref || github.ref }} - name: Run Nerdbank.GitVersioning tool step id: nbgv diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 7d9decfe..0893561e 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -12,8 +12,11 @@ on: schedule: - cron: '0 2 * * MON' +# Single global group (not ref-scoped): this workflow pushes shared tags +# (base images, both branches, readme, badge), so two dispatches on +# different refs must not run concurrently and race on those shared tags. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }} cancel-in-progress: true jobs: @@ -32,6 +35,9 @@ jobs: secrets: inherit with: push: true + # Always build the shared nx-base tag from main, even when dispatched + # from another ref, so a develop dispatch can't overwrite main's base. + ref: main build-main: name: Build main images job diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 87a4855f..94ad678a 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -15,10 +15,16 @@ jobs: name: Detect image changes job runs-on: ubuntu-latest permissions: + contents: read pull-requests: read outputs: image: ${{ steps.filter.outputs.image }} steps: + # Checkout so paths-filter can diff via git for non-PR triggers + # (e.g. workflow_dispatch); on pull_request it uses the API. + - name: Checkout step + uses: actions/checkout@v6 + - name: Filter changed paths step id: filter uses: dorny/paths-filter@v3 From 6c44797af753c6608d6f3d1fa1b14a86102d4834 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 08:56:20 -0700 Subject: [PATCH 04/13] Address Copilot re-review: pin release to main, gate release, status check - publish-release: compute the version from main (get-version ref: main) and check out main for the GitHub release, so release metadata/assets match the main publish regardless of the dispatch ref; set prerelease false accordingly. - publish-release: github-release now needs both build-main and build-develop, so no release is published when develop is still building or has failed. - test-pull-request: check-workflow-status now needs the changes job and fails if it fails/cancels, so a paths-filter failure can't let an image-changing PR merge with the smoke build silently skipped. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish-release.yml | 13 +++++++++++-- .github/workflows/test-pull-request.yml | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 0893561e..eebd2e36 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -21,10 +21,14 @@ concurrency: jobs: + # Pin the release version to main: this run's GitHub release tracks the + # main publish, so compute it from main even when dispatched from another ref. get-version: name: Get version information job uses: ./.github/workflows/get-version-task.yml secrets: inherit + with: + ref: main # Base images share a single tag (nx-base:ubuntu-noble) across branches, # so build them once (from the release branch) and let both branch legs @@ -59,9 +63,11 @@ jobs: ref: develop build_base: false + # Gate on both build legs so a release is never published when develop + # is still building or failed. Release metadata/assets track main. github-release: name: Publish GitHub release job - needs: [get-version, build-main] + needs: [get-version, build-main, build-develop] runs-on: ubuntu-latest permissions: contents: write @@ -70,13 +76,16 @@ jobs: - name: Checkout code step uses: actions/checkout@v6 + with: + ref: main - name: Create GitHub release step uses: softprops/action-gh-release@v3 with: generate_release_notes: true tag_name: ${{ needs.get-version.outputs.SemVer2 }} - prerelease: ${{ github.ref_name != 'main' }} + # This run's release represents the main publish. + prerelease: false files: | LICENSE README.md diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 94ad678a..642ae074 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -61,7 +61,7 @@ jobs: name: Check pull request workflow status runs-on: ubuntu-latest needs: - [ test-release, smoke-build ] + [ changes, test-release, smoke-build ] if: always() steps: - name: Check workflow results @@ -74,5 +74,9 @@ jobs: exit 1 fi } + # changes must succeed: if it fails, smoke-build is skipped (not + # run), and treating that skip as a pass would let an + # image-changing PR merge without the smoke build. + exit_on_result "changes" "${{ needs.changes.result }}" exit_on_result "test-release" "${{ needs.test-release.result }}" exit_on_result "smoke-build" "${{ needs.smoke-build.result }}" From a3d77b05e4775ca19314428988604dc4bb2623c6 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:02:05 -0700 Subject: [PATCH 05/13] Address Copilot re-review: base push/cache key on built ref not trigger build-base-images-task pushed and wrote caches only when github.ref_name was main/develop, but the publisher now builds the shared base from ref: main and may be dispatched from another branch. Drop the ref_name guard so push is controlled by the caller (inputs.push), and scope the cache to the built ref (inputs.ref) instead of github.ref_name. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-base-images-task.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-base-images-task.yml b/.github/workflows/build-base-images-task.yml index b197dfd2..847288fd 100644 --- a/.github/workflows/build-base-images-task.yml +++ b/.github/workflows/build-base-images-task.yml @@ -70,7 +70,11 @@ jobs: - name: Docker build and push base step uses: docker/build-push-action@v7 with: - push: ${{ inputs.push && (github.ref_name == 'main' || github.ref_name == 'develop') }} + # Push is controlled by the caller, not the trigger ref: the + # publisher passes push: true with ref: main, so it must publish + # the shared base tag even when dispatched from another branch. + # Cache scopes key on the built ref, not github.ref_name. + push: ${{ inputs.push }} context: Docker file: ${{ matrix.base.dockerfile }} platforms: ${{ inputs.platforms }} @@ -81,6 +85,6 @@ jobs: type=gha,scope=main-base-${{ matrix.base.name }} ${{ github.event.pull_request && format('type=gha,scope=pr-base-{0}-{1}', github.event.pull_request.number, matrix.base.name) || '' }} cache-to: | - ${{ inputs.push && (github.ref_name == 'main' || github.ref_name == 'develop') && format('type=gha,mode=max,scope={0}-base-{1},ignore-error=true', github.ref_name, matrix.base.name) || '' }} - ${{ inputs.push && (github.ref_name == 'main' || github.ref_name == 'develop') && 'type=inline' || '' }} + ${{ inputs.push && format('type=gha,mode=max,scope={0}-base-{1},ignore-error=true', inputs.ref != '' && inputs.ref || github.ref_name, matrix.base.name) || '' }} + ${{ inputs.push && 'type=inline' || '' }} ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork && format('type=gha,mode=max,scope=pr-base-{0}-{1},ignore-error=true', github.event.pull_request.number, matrix.base.name) || '' }} From 71cea50c9fe407c3fb26c8a717bcb7990a750405 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:06:14 -0700 Subject: [PATCH 06/13] Document Copilot re-review trigger (requestReviews botIds) in AGENTS.md Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 4a56ce66..99a83731 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,10 @@ For comprehensive coding and formatting standards, follow: - Open PRs against `develop` (the integration branch); `develop` is forward-only and ships to `main` via release merges. - The repo is configured to automatically request a GitHub Copilot review when a PR is opened. Respond to every Copilot comment: either address it with a change, or justify why it does not apply. Either way, reply on the comment stating what you did, then resolve (close) the comment. -- After you push a new commit, the automatic Copilot re-review is flaky and often does not fire. If you cannot trigger a re-review, ask the maintainer to start one in the GitHub UI. Repeat until both Copilot and the author are satisfied. +- After you push a new commit, a Copilot re-review does not reliably fire on its own. In practice the re-review can be requested via the GraphQL `requestReviews` mutation, passing the Copilot bot's node id in `botIds`: + - Get the bot id once from the existing review author: `pullRequest { reviews(first:1){ nodes { author { ... on Bot { id } } } } }` (the Copilot reviewer login is `copilot-pull-request-reviewer`). + - Trigger: `mutation { requestReviews(input: {pullRequestId: "", botIds: [""], union: true}) { pullRequest { id } } }`. + - This is observed-but-not-guaranteed; if a re-review still does not appear, ask the maintainer to start one in the GitHub UI. Repeat until both Copilot and the author are satisfied. ## Coding Conventions (Highlights) From 81b259edbba7b11126d9ff3994936c14fe244386 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:12:11 -0700 Subject: [PATCH 07/13] Address Copilot re-review: pin readme to main, always update date badge - publish-docker-readme-task: add a ref input and check out that ref; the publisher passes ref: main so published Docker Hub readmes and the repo list track main content, consistent with the pinned version/release. - build-datebadge-task: drop the github.ref_name == 'main' gate. The sole caller (publish-release) runs it only after a successful publish and may be dispatched from any ref, so the Last Build badge should always update. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-datebadge-task.yml | 4 +++- .github/workflows/publish-docker-readme-task.yml | 12 ++++++++++++ .github/workflows/publish-release.yml | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index bb0edfe8..724a3390 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -16,8 +16,10 @@ jobs: run: | echo "date=$(date)" >> "$GITHUB_OUTPUT" + # No ref gate: the sole caller (publish-release) only runs this after + # a successful publish and can be dispatched from any ref, so the + # "Last Build" badge should update on every publish. - name: Build BYOB date badge step - if: ${{ github.ref_name == 'main' }} uses: RubbaBoy/BYOB@v1 with: name: lastbuild diff --git a/.github/workflows/publish-docker-readme-task.yml b/.github/workflows/publish-docker-readme-task.yml index 2d6706a7..e970950b 100644 --- a/.github/workflows/publish-docker-readme-task.yml +++ b/.github/workflows/publish-docker-readme-task.yml @@ -2,6 +2,14 @@ name: Publish docker hub readme task on: workflow_call: + inputs: + # Branch whose Matrix.json / Docker/README.md drive the published + # readmes. The publisher passes main so readme content matches the + # main release artifacts even when dispatched from another ref. + ref: + required: false + type: string + default: '' jobs: @@ -15,6 +23,8 @@ jobs: - name: Checkout step uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref }} - name: Load matrix.json step id: getrepos @@ -33,6 +43,8 @@ jobs: - name: Checkout step uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref }} - name: Update docker hub description step uses: peter-evans/dockerhub-description@v5 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index eebd2e36..c4fec5fa 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -97,6 +97,9 @@ jobs: needs: [build-main, build-develop] uses: ./.github/workflows/publish-docker-readme-task.yml secrets: inherit + with: + # Readme content tracks main, consistent with the version/release. + ref: main date-badge: name: Create BYOB date badge job From f947218130fd52060dbd42162d1b9f27fbbb3d85 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:18:24 -0700 Subject: [PATCH 08/13] Address Copilot re-review: only build base in smoke when base changed The PR smoke product build pulls the published base from Docker Hub, so building the base on every image-changing PR was wasted work. Add a base paths-filter (Docker/NxBase*.Dockerfile) and pass build_base to the smoke build only when a base Dockerfile changed, so a base bump is still compile-validated while ordinary product/version PRs skip the base build. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test-pull-request.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 642ae074..e831dbe2 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -19,6 +19,7 @@ jobs: pull-requests: read outputs: image: ${{ steps.filter.outputs.image }} + base: ${{ steps.filter.outputs.base }} steps: # Checkout so paths-filter can diff via git for non-PR triggers # (e.g. workflow_dispatch); on pull_request it uses the API. @@ -34,6 +35,9 @@ jobs: - 'Docker/**' - 'Make/Matrix.json' - 'Make/Version.json' + base: + - 'Docker/NxBase.Dockerfile' + - 'Docker/NxBase-LSIO.Dockerfile' test-release: name: Test release job @@ -45,6 +49,11 @@ jobs: # See build-docker-task.yml `smoke`. Publishing happens only on the # scheduled publish-release.yml, so a smoke pass is fast PR feedback, # not a publish gate. + # + # build_base is only enabled when a base Dockerfile changed: the product + # smoke build pulls the published base from Docker Hub, so building the + # base otherwise is wasted work. When a base Dockerfile does change, build + # it to validate it still compiles (e.g. a Dependabot ubuntu:noble bump). smoke-build: name: Smoke build job needs: [changes] @@ -54,6 +63,7 @@ jobs: with: push: false smoke: true + build_base: ${{ needs.changes.outputs.base == 'true' }} # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 From 6a45f712a436edcc7438d4b76820f904a388d108 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:23:48 -0700 Subject: [PATCH 09/13] Address Copilot re-review: full history for paths-filter changes job dorny/paths-filter falls back to git diff for non-PR triggers (workflow_dispatch); the default shallow checkout can miss the history it needs. Use fetch-depth: 0 so those runs are deterministic. PRs use the API and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test-pull-request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index e831dbe2..a3b5cb0f 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -23,8 +23,11 @@ jobs: steps: # Checkout so paths-filter can diff via git for non-PR triggers # (e.g. workflow_dispatch); on pull_request it uses the API. + # fetch-depth: 0 gives the full history those git diffs need. - name: Checkout step uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Filter changed paths step id: filter From eb2729d466ee84ffcfe80f40a840aa43998db994 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:30:08 -0700 Subject: [PATCH 10/13] Address Copilot re-review: queue publishes instead of cancelling Set publish-release concurrency cancel-in-progress: false so a manual dispatch during an in-flight (scheduled or manual) publish queues rather than cancelling it, avoiding a partially pushed set of shared tags. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish-release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c4fec5fa..8861a4aa 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -15,9 +15,11 @@ on: # Single global group (not ref-scoped): this workflow pushes shared tags # (base images, both branches, readme, badge), so two dispatches on # different refs must not run concurrently and race on those shared tags. +# cancel-in-progress: false queues the next run instead of cancelling an +# in-flight publish, which could otherwise leave a partially pushed tag set. concurrency: group: ${{ github.workflow }} - cancel-in-progress: true + cancel-in-progress: false jobs: From e1d8fd421e363f1f6afe76a62be4cf8eae2df709 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:36:44 -0700 Subject: [PATCH 11/13] Address Copilot re-review: pin GitHub release target to main action-gh-release tags against the run commit (github.sha) unless target_commitish is set; since the publisher is dispatchable from any ref, set target_commitish: main so the release tag lands on main, consistent with the main-pinned version and assets. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 8861a4aa..8b96213e 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -86,6 +86,9 @@ jobs: with: generate_release_notes: true tag_name: ${{ needs.get-version.outputs.SemVer2 }} + # Pin the tag to main's HEAD so a dispatch from another ref still + # tags the release on main, matching the main-pinned version/assets. + target_commitish: main # This run's release represents the main publish. prerelease: false files: | From abec4f091f44e816ea22624d0dfdc19bca8f6d44 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:44:03 -0700 Subject: [PATCH 12/13] Address Copilot re-review: smoke covers both branches, sync README version - build-docker-task smoke filter: de-dup by [.Name, .Branch] instead of .Name, so the smoke build covers NxMeta/NxMeta-LSIO for both main and develop (4 amd64 builds), and a develop-targeted PR validates the develop rows/args rather than only the main rows. - README: bump the Release Notes header to 2.12 with a CI-rework summary to match version.json / HISTORY.md, and drop the now-unused nxpackage-link / nxvmsdockerfile-link reference definitions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-docker-task.yml | 8 +++++--- README.md | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index 070d54ed..104e4ed0 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -55,9 +55,11 @@ jobs: # variable, so it must stay single-quoted / unexpanded. # shellcheck disable=SC2016 if [[ "$SMOKE" == "true" ]]; then - # One Ubuntu + one LSIO variant exercises the shared Dockerfile - # build logic; tags/args are irrelevant since smoke never pushes. - FILTER='.Images |= (map(select(.Name == "NxMeta" or .Name == "NxMeta-LSIO")) | unique_by(.Name))' + # One Ubuntu + one LSIO variant per branch exercises the shared + # Dockerfile build logic and each branch's build args; tags are + # irrelevant since smoke never pushes. De-dup by name+branch so a + # PR targeting develop still validates the develop rows. + FILTER='.Images |= (map(select(.Name == "NxMeta" or .Name == "NxMeta-LSIO")) | unique_by([.Name, .Branch]))' elif [[ -n "$REF" ]]; then # Publish: build only the rows targeting the branch being built # (avoids building the other branch's rows just to discard them). diff --git a/README.md b/README.md index 6ec67171..41acae66 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ This is a project to build and publish docker images for various [Network Optix] ### Release Notes -**Version: 2.11**: +**Version: 2.12**: **Summary**: -- Add `currentOsVariantOverride=docker` to `mediaserver.conf` following the pattern used in the current Nx [Dockerfile][nxvmsdockerfile-link], and documented in this [support][nxpackage-link] article. +- Reworked the CI pipeline: pull requests run a fast representative amd64 smoke build (`NxMeta` and `NxMeta-LSIO`) instead of the full matrix, publishing moved to a weekly schedule (and manual trigger) that builds both the `main` and `develop` branches in one run, and merges no longer republish images. See [Release History](./HISTORY.md) for complete release notes and older versions. @@ -713,10 +713,8 @@ Licensed under the [MIT License][license-link]\ [nxmetadownload-link]: https://meta.nxvms.com/download/linux [nxmetareleases-link]: https://updates.vmsproxy.com/metavms/releases.json [nxossupport-link]: https://support.networkoptix.com/hc/en-us/articles/205313168-Nx-Witness-Operating-System-Support -[nxpackage-link]: https://support.networkoptix.com/hc/en-us/articles/32917149024535-Creating-Update-Packages-for-Custom-Linux-and-or-ARM-Servers [nxreleasenotes-link]: https://support.networkoptix.com/hc/en-us/articles/360042751193-Current-and-Past-Releases-Downloads-Release-Notes [nxsupport-link]: https://support.networkoptix.com/hc/en-us/community/topics -[nxvmsdockerfile-link]: https://github.com/networkoptix/nxvms-docker/blob/master/Dockerfile [nxwebadmin-link]: https://support.networkoptix.com/hc/en-us/articles/115012831028-Nx-Server-Web-Admin [nxwitness-link]: https://www.networkoptix.com/nx-witness/ [nxwitnessbetadownload-link]: https://beta.networkoptix.com/beta-builds/default From 671d395fff08146461d84f0f7cf9cfd14a4e4fa5 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 09:55:40 -0700 Subject: [PATCH 13/13] Configure workspace linting for structured files via recommended extensions Add .vscode/extensions.json recommending linters for every structured file type the repo uses -- C# (Roslyn + CSharpier), Markdown (markdownlint), YAML (Red Hat schema), Dockerfiles/Compose (Docker), and GitHub Actions (GitHub Actions extension) -- so a fresh clone gets editor linting for all of them rather than singling out workflows or adding a CI lint job. Document the approach in AGENTS.md (new "Linting structured files" section) and copilot-instructions.md; actionlint remains the deeper CLI check for workflow edits. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/copilot-instructions.md | 3 ++- .vscode/extensions.json | 17 +++++++++++++++++ AGENTS.md | 11 ++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9b20856b..4fa4d55f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,7 +42,8 @@ This repository builds and publishes Docker images for Network Optix VMS product - Pull requests run unit tests and style checks, plus a fast smoke build (NxMeta and NxMeta-LSIO, amd64 only, no push) that runs only when image files change -- not the full matrix. - Publishing is schedule/manual only via `publish-release.yml`, which builds the base images once and then publishes the full matrix for both the `main` and `develop` branches in a single run. -- Merges to `main`/`develop` do not publish; auto-merged Dependabot and codegen PRs are picked up by the next scheduled publish. Do not reintroduce push-triggered publishing or full-matrix PR builds. Validate workflow edits with `actionlint`. +- Merges to `main`/`develop` do not publish; auto-merged Dependabot and codegen PRs are picked up by the next scheduled publish. Do not reintroduce push-triggered publishing or full-matrix PR builds. +- Structured files are linted in-editor via the workspace-recommended extensions in [.vscode/extensions.json](../.vscode/extensions.json) (C#, Markdown, YAML, Docker, GitHub Actions) rather than a CI lint job; lint changed files before pushing, and run `actionlint` for deeper workflow checks. See AGENTS.md. ## What to Keep in Sync diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..ac104794 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,17 @@ +{ + "recommendations": [ + // C#: language server + formatter (also enforced by Husky/dotnet format) + "ms-dotnettools.csharp", + "csharpier.csharpier-vscode", + // Shared editor/format conventions from .editorconfig + "editorconfig.editorconfig", + // Markdown linting (README.md, HISTORY.md, docs) + "davidanson.vscode-markdownlint", + // YAML schema validation (compose files, workflow YAML) + "redhat.vscode-yaml", + // Dockerfile and Docker Compose linting + "ms-azuretools.vscode-docker", + // GitHub Actions workflow validation and expression linting + "github.vscode-github-actions" + ] +} diff --git a/AGENTS.md b/AGENTS.md index 99a83731..b683a9af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,15 @@ For comprehensive coding and formatting standards, follow: - C# code should be formatted with CSharpier, then verified with `dotnet format` (style). - The `.Net Format` VS Code task in `.vscode/tasks.json` must be clean and warning-free at all times. +### Linting structured files + +- Every structured file type has a linter configured in the workspace via the recommended VS Code extensions in `.vscode/extensions.json`; there is no separate CI lint job. Lint changed files and clear reported problems before pushing. Coverage: + - C# — Roslyn analyzers + CSharpier (also enforced by Husky/`dotnet format`). + - Markdown — markdownlint (`README.md`, `HISTORY.md`, docs). + - YAML — Red Hat YAML schema validation (compose and workflow files). + - Dockerfiles / Docker Compose — the Docker extension. + - GitHub Actions workflows — the GitHub Actions extension (schema + expression checks); for deeper checks (including shellcheck on `run:` steps) run the `actionlint` CLI. + ## Image Architecture - Base images (`nx-base`, `nx-base-lsio`) are built and pushed, then reused as `FROM` images for derived product Dockerfiles. @@ -51,7 +60,7 @@ For comprehensive coding and formatting standards, follow: - Pull requests (`test-pull-request.yml`) run unit tests and code style, plus a fast smoke build only when image files (`Docker/**`, `Make/Matrix.json`, `Make/Version.json`) change. The smoke build (`build-docker-task.yml` with `smoke: true`) builds a representative subset (NxMeta and NxMeta-LSIO, amd64 only, no push), not the full matrix. - Publishing happens only on a schedule or manual dispatch (`publish-release.yml`): it builds the base images once, then builds and pushes the full matrix for both the `main` and `develop` branches (`build-docker-task.yml` with `ref:` and `build_base: false`), and updates the GitHub release, Docker Hub readme, and date badge. - Merges to `main`/`develop` do not build or publish images. Auto-merged Dependabot and codegen PRs simply land commits that the next scheduled publish picks up. Do not reintroduce push-triggered publishing or full-matrix PR builds. -- Validate workflow edits with `actionlint` before pushing. +- Lint workflow edits before pushing (see [Linting structured files](#linting-structured-files)); there is no CI lint job. ## Pull Request Review Process