diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fa9f57ca..4fa4d55f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,6 +38,13 @@ 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. +- 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 - 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..847288fd 100644 --- a/.github/workflows/build-base-images-task.yml +++ b/.github/workflows/build-base-images-task.yml @@ -7,6 +7,17 @@ on: required: false type: boolean default: true + platforms: + 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: @@ -32,18 +43,24 @@ jobs: - name: Checkout step uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref }} - 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 }} + # 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 @@ -53,10 +70,14 @@ 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: linux/amd64,linux/arm64 + platforms: ${{ inputs.platforms }} tags: ${{ matrix.base.tags }} cache-from: | type=registry,ref=${{ matrix.base.cache_tag }} @@ -64,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) || '' }} diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index 8f7f8f8d..724a3390 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -14,10 +14,12 @@ jobs: - name: Get current date step id: date run: | - echo "date=$(date)" >> $GITHUB_OUTPUT + 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/build-docker-task.yml b/.github/workflows/build-docker-task.yml index f5bc63b6..104e4ed0 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -1,104 +1,168 @@ -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 || github.ref }} + + - name: Load matrix.json step + id: getmatrix + env: + 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 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). + # $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 --arg ref "$REF" --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' }} + ref: ${{ inputs.ref }} + 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 || github.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' }} + + # 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 + 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..ed9b7898 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 || github.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-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-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..8b96213e 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,36 +1,115 @@ -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' + +# 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: false + +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 + # 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 + # 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 + 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 + + # 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, build-develop] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + + - 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 }} + # 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: | + 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 + with: + # Readme content tracks main, consistent with the version/release. + ref: main + + 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..a3b5cb0f 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,36 +1,95 @@ -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: + contents: read + 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. + # 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 + uses: dorny/paths-filter@v3 + with: + filters: | + image: + - 'Docker/**' + - 'Make/Matrix.json' + - 'Make/Version.json' + base: + - 'Docker/NxBase.Dockerfile' + - 'Docker/NxBase-LSIO.Dockerfile' + + 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. + # + # 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] + if: ${{ needs.changes.outputs.image == 'true' }} + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + 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 + check-workflow-status: + name: Check pull request workflow status + runs-on: ubuntu-latest + needs: + [ changes, 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 + } + # 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 }}" 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/.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 6cda8d85..b683a9af 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,46 @@ 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. + +### 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. +- 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. +- Lint workflow edits before pushing (see [Linting structured files](#linting-structured-files)); there is no CI lint job. + +## 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, 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) @@ -54,8 +79,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..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. @@ -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**: @@ -711,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 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$" ],