From 65f2f071a78307fbcfd3750ffa688b0fca341fad Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 8 Jun 2026 12:08:44 -0700 Subject: [PATCH 01/12] Bump version to 2.13 for next prerelease cycle (#425) Isolated version bump establishing the develop-leads invariant: raises `develop`'s minor from `2.12` to `2.13` so develop's NBGV prereleases sort above main's last stable `2.12.x`. Standalone version-only change per the versioning policy. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2f7892b..388dbef 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.12", + "version": "2.13", "publicReleaseRefSpec": [ "^refs/heads/main$" ], From f5a8ebabfe87cbc25275b311526e69c4e87f685e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 8 Jun 2026 12:12:31 -0700 Subject: [PATCH 02/12] Document the develop-leads versioning policy (#424) Propagates the versioning rule from ptr727/ProjectTemplate: `develop` leads `main` by a minor (after a `develop -> main` release, bump develop's minor via an isolated `bump-version-X.Y` PR so develop's prerelease image tags sort above main's stable), and maintenance promotions hold main's version. Added a Versioning section to AGENTS.md and `.github/copilot-instructions.md". --- .github/copilot-instructions.md | 4 ++++ AGENTS.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fd3bb0d..2c7434a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -57,6 +57,10 @@ This repository builds and publishes Docker images for Network Optix VMS product - Use explicit types (no `var`), Allman braces, file-scoped namespaces, and other conventions as defined in the master style guide. - Respect line endings and encoding rules from the repository configuration, including UTF-8 without BOM. +## Versioning + +`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR, so develop's NBGV prerelease version (baked into its images as `LABEL_VERSION`) stays above main's last stable release. A `develop -> main` promotion that carries only maintenance (dependency bumps, CI/doc fixes, template re-syncs) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Versioning"](../AGENTS.md#versioning). + ## GitHub Copilot Review Runbook Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. diff --git a/AGENTS.md b/AGENTS.md index 24129a5..149c55a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,10 @@ 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. - Lint workflow edits before pushing (see [Workspace and linting](#workspace-and-linting)); there is no CI lint job. +## Versioning + +The `version` (major.minor) in [version.json](./version.json) is the NBGV version floor; NBGV appends the git height. **`develop` leads `main` by a minor:** after a `develop -> main` release lands and main's publish completes, bump the minor in `version.json` on `develop` in an isolated `bump-version-X.Y` PR (X.Y = the new minor), so develop's NBGV prerelease version stays numerically above main's last stable. A **maintenance** `develop -> main` promotion (dependency bumps, CI/doc fixes, template re-syncs) holds main's version - `git checkout main -- version.json` on the promotion branch - so `main` advances only its NBGV height, not its minor. (NBGV's version is the GitHub release tag on `main` and the `LABEL_VERSION` build arg baked into the images; the Docker image *tags* carry the Nx product version from `Make/Matrix.json` - see [CI Pipeline](#ci-pipeline-github-actions).) + ## PR Review Etiquette The repo runs a review loop on every PR: local agent iteration plus remote automated review (GitHub Copilot is the configured reviewer). Treat this as a contract regardless of which local agent authored the changes. From 8f8569a53bbe56cb96eed32b9a4f95626d3c0de9 Mon Sep 17 00:00:00 2001 From: "ptr727-codegen[bot]" <275599072+ptr727-codegen[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:06:00 +0000 Subject: [PATCH 03/12] Update codegen files (#427) This PR updates the codegen files. Co-authored-by: ptr727-codegen[bot] <275599072+ptr727-codegen[bot]@users.noreply.github.com> --- Unraid/DWSpectrumLSIO.xml | 9 +++------ Unraid/NxMetaLSIO.xml | 9 +++------ Unraid/NxWitnessLSIO.xml | 9 +++------ 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Unraid/DWSpectrumLSIO.xml b/Unraid/DWSpectrumLSIO.xml index d042935..3bee4c4 100644 --- a/Unraid/DWSpectrumLSIO.xml +++ b/Unraid/DWSpectrumLSIO.xml @@ -48,8 +48,7 @@ Display="always" Required="false" Mask="false" - >99 + >99 100 + >100 /mnt/user/appdata/dwspectrum-lsio + >/mnt/user/appdata/dwspectrum-lsio 99 + >99 100 + >100 /mnt/user/appdata/nxmeta-lsio + >/mnt/user/appdata/nxmeta-lsio 99 + >99 100 + >100 /mnt/user/appdata/nxwitness-lsio + >/mnt/user/appdata/nxwitness-lsio Date: Wed, 10 Jun 2026 03:08:24 +0000 Subject: [PATCH 04/12] Update codegen files (#428) This PR updates the codegen files. Co-authored-by: ptr727-codegen[bot] <275599072+ptr727-codegen[bot]@users.noreply.github.com> --- Make/Matrix.json | 248 ++++++++++++++++++---------------------------- Make/Version.json | 39 +++----- 2 files changed, 112 insertions(+), 175 deletions(-) diff --git a/Make/Matrix.json b/Make/Matrix.json index 557e7ca..7194f01 100644 --- a/Make/Matrix.json +++ b/Make/Matrix.json @@ -1,21 +1,6 @@ { "SchemaVersion": 2, "Images": [ - { - "Name": "NxGo", - "Product": "nxgo", - "Branch": "main", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxgo:6.1.1.42624", - "docker.io/ptr727/nxgo:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip" - ] - }, { "Name": "NxGo", "Product": "nxgo", @@ -23,7 +8,8 @@ "Base": "ubuntu", "Tags": [ "docker.io/ptr727/nxgo:6.1.2.42921", - "docker.io/ptr727/nxgo:latest" + "docker.io/ptr727/nxgo:latest", + "docker.io/ptr727/nxgo:stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", @@ -31,21 +17,6 @@ "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" ] }, - { - "Name": "NxGo", - "Product": "nxgo", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxgo:develop-6.1.1.42624", - "docker.io/ptr727/nxgo:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip" - ] - }, { "Name": "NxGo", "Product": "nxgo", @@ -53,7 +24,8 @@ "Base": "ubuntu", "Tags": [ "docker.io/ptr727/nxgo:develop", - "docker.io/ptr727/nxgo:develop-6.1.2.42921" + "docker.io/ptr727/nxgo:develop-6.1.2.42921", + "docker.io/ptr727/nxgo:develop-stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", @@ -67,23 +39,25 @@ "Branch": "main", "Base": "lsio", "Tags": [ - "docker.io/ptr727/nxgo-lsio:6.1.1.42624", + "docker.io/ptr727/nxgo-lsio:6.1.2.42921", + "docker.io/ptr727/nxgo-lsio:latest", "docker.io/ptr727/nxgo-lsio:stable" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" ] }, { "Name": "NxGo-LSIO", "Product": "nxgo", - "Branch": "main", + "Branch": "develop", "Base": "lsio", "Tags": [ - "docker.io/ptr727/nxgo-lsio:6.1.2.42921", - "docker.io/ptr727/nxgo-lsio:latest" + "docker.io/ptr727/nxgo-lsio:develop", + "docker.io/ptr727/nxgo-lsio:develop-6.1.2.42921", + "docker.io/ptr727/nxgo-lsio:develop-stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", @@ -92,49 +66,48 @@ ] }, { - "Name": "NxGo-LSIO", - "Product": "nxgo", - "Branch": "develop", - "Base": "lsio", + "Name": "NxMeta", + "Product": "nxmeta", + "Branch": "main", + "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/nxgo-lsio:develop-6.1.1.42624", - "docker.io/ptr727/nxgo-lsio:develop-stable" + "docker.io/ptr727/nxmeta:6.1.1.42649", + "docker.io/ptr727/nxmeta:latest" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42649", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip" ] }, { - "Name": "NxGo-LSIO", - "Product": "nxgo", - "Branch": "develop", - "Base": "lsio", + "Name": "NxMeta", + "Product": "nxmeta", + "Branch": "main", + "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/nxgo-lsio:develop", - "docker.io/ptr727/nxgo-lsio:develop-6.1.2.42921" + "docker.io/ptr727/nxmeta:6.1.2.42921", + "docker.io/ptr727/nxmeta:stable" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" ] }, { "Name": "NxMeta", "Product": "nxmeta", - "Branch": "main", + "Branch": "develop", "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/nxmeta:6.1.1.42624", - "docker.io/ptr727/nxmeta:latest", - "docker.io/ptr727/nxmeta:stable" + "docker.io/ptr727/nxmeta:develop", + "docker.io/ptr727/nxmeta:develop-6.1.1.42649" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42649", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip" ] }, { @@ -143,14 +116,28 @@ "Branch": "develop", "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/nxmeta:develop", - "docker.io/ptr727/nxmeta:develop-6.1.1.42624", + "docker.io/ptr727/nxmeta:develop-6.1.2.42921", "docker.io/ptr727/nxmeta:develop-stable" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" + ] + }, + { + "Name": "NxMeta-LSIO", + "Product": "nxmeta", + "Branch": "main", + "Base": "lsio", + "Tags": [ + "docker.io/ptr727/nxmeta-lsio:6.1.1.42649", + "docker.io/ptr727/nxmeta-lsio:latest" + ], + "Args": [ + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42649", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip" ] }, { @@ -159,14 +146,13 @@ "Branch": "main", "Base": "lsio", "Tags": [ - "docker.io/ptr727/nxmeta-lsio:6.1.1.42624", - "docker.io/ptr727/nxmeta-lsio:latest", + "docker.io/ptr727/nxmeta-lsio:6.1.2.42921", "docker.io/ptr727/nxmeta-lsio:stable" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" ] }, { @@ -176,28 +162,27 @@ "Base": "lsio", "Tags": [ "docker.io/ptr727/nxmeta-lsio:develop", - "docker.io/ptr727/nxmeta-lsio:develop-6.1.1.42624", - "docker.io/ptr727/nxmeta-lsio:develop-stable" + "docker.io/ptr727/nxmeta-lsio:develop-6.1.1.42649" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42649", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip" ] }, { - "Name": "NxWitness", - "Product": "nxwitness", - "Branch": "main", - "Base": "ubuntu", + "Name": "NxMeta-LSIO", + "Product": "nxmeta", + "Branch": "develop", + "Base": "lsio", "Tags": [ - "docker.io/ptr727/nxwitness:6.1.1.42624", - "docker.io/ptr727/nxwitness:stable" + "docker.io/ptr727/nxmeta-lsio:develop-6.1.2.42921", + "docker.io/ptr727/nxmeta-lsio:develop-stable" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42921", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip" ] }, { @@ -207,7 +192,8 @@ "Base": "ubuntu", "Tags": [ "docker.io/ptr727/nxwitness:6.1.2.42921", - "docker.io/ptr727/nxwitness:latest" + "docker.io/ptr727/nxwitness:latest", + "docker.io/ptr727/nxwitness:stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", @@ -215,21 +201,6 @@ "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" ] }, - { - "Name": "NxWitness", - "Product": "nxwitness", - "Branch": "develop", - "Base": "ubuntu", - "Tags": [ - "docker.io/ptr727/nxwitness:develop-6.1.1.42624", - "docker.io/ptr727/nxwitness:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip" - ] - }, { "Name": "NxWitness", "Product": "nxwitness", @@ -237,7 +208,8 @@ "Base": "ubuntu", "Tags": [ "docker.io/ptr727/nxwitness:develop", - "docker.io/ptr727/nxwitness:develop-6.1.2.42921" + "docker.io/ptr727/nxwitness:develop-6.1.2.42921", + "docker.io/ptr727/nxwitness:develop-stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", @@ -245,21 +217,6 @@ "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" ] }, - { - "Name": "NxWitness-LSIO", - "Product": "nxwitness", - "Branch": "main", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxwitness-lsio:6.1.1.42624", - "docker.io/ptr727/nxwitness-lsio:stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip" - ] - }, { "Name": "NxWitness-LSIO", "Product": "nxwitness", @@ -267,7 +224,8 @@ "Base": "lsio", "Tags": [ "docker.io/ptr727/nxwitness-lsio:6.1.2.42921", - "docker.io/ptr727/nxwitness-lsio:latest" + "docker.io/ptr727/nxwitness-lsio:latest", + "docker.io/ptr727/nxwitness-lsio:stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", @@ -275,21 +233,6 @@ "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" ] }, - { - "Name": "NxWitness-LSIO", - "Product": "nxwitness", - "Branch": "develop", - "Base": "lsio", - "Tags": [ - "docker.io/ptr727/nxwitness-lsio:develop-6.1.1.42624", - "docker.io/ptr727/nxwitness-lsio:develop-stable" - ], - "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.1.42624", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip" - ] - }, { "Name": "NxWitness-LSIO", "Product": "nxwitness", @@ -297,7 +240,8 @@ "Base": "lsio", "Tags": [ "docker.io/ptr727/nxwitness-lsio:develop", - "docker.io/ptr727/nxwitness-lsio:develop-6.1.2.42921" + "docker.io/ptr727/nxwitness-lsio:develop-6.1.2.42921", + "docker.io/ptr727/nxwitness-lsio:develop-stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", @@ -326,13 +270,13 @@ "Branch": "main", "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/dwspectrum:6.1.2.42921", + "docker.io/ptr727/dwspectrum:6.1.2.42997", "docker.io/ptr727/dwspectrum:latest" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42997", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip" ] }, { @@ -357,12 +301,12 @@ "Base": "ubuntu", "Tags": [ "docker.io/ptr727/dwspectrum:develop", - "docker.io/ptr727/dwspectrum:develop-6.1.2.42921" + "docker.io/ptr727/dwspectrum:develop-6.1.2.42997" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42997", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip" ] }, { @@ -386,13 +330,13 @@ "Branch": "main", "Base": "lsio", "Tags": [ - "docker.io/ptr727/dwspectrum-lsio:6.1.2.42921", + "docker.io/ptr727/dwspectrum-lsio:6.1.2.42997", "docker.io/ptr727/dwspectrum-lsio:latest" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42997", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip" ] }, { @@ -417,12 +361,12 @@ "Base": "lsio", "Tags": [ "docker.io/ptr727/dwspectrum-lsio:develop", - "docker.io/ptr727/dwspectrum-lsio:develop-6.1.2.42921" + "docker.io/ptr727/dwspectrum-lsio:develop-6.1.2.42997" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_arm64.zip", - "DOWNLOAD_VERSION=6.1.2.42921", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.2.42997", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip" ] }, { diff --git a/Make/Version.json b/Make/Version.json index 7435eee..827aaa3 100644 --- a/Make/Version.json +++ b/Make/Version.json @@ -4,19 +4,12 @@ { "Product": "NxGo", "Versions": [ - { - "Version": "6.1.1.42624", - "UriX64": "https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, { "Version": "6.1.2.42921", "UriX64": "https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip", "UriArm64": "https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip", "Labels": [ + "Stable", "Latest" ] } @@ -26,32 +19,32 @@ "Product": "NxMeta", "Versions": [ { - "Version": "6.1.1.42624", - "UriX64": "https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip", + "Version": "6.1.1.42649", + "UriX64": "https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip", "Labels": [ - "Stable", "Latest" ] + }, + { + "Version": "6.1.2.42921", + "UriX64": "https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/metavms/42921/metavms-server_update-6.1.2.42921-linux_arm64.zip", + "Labels": [ + "Stable" + ] } ] }, { "Product": "NxWitness", "Versions": [ - { - "Version": "6.1.1.42624", - "UriX64": "https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip", - "Labels": [ - "Stable" - ] - }, { "Version": "6.1.2.42921", "UriX64": "https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip", "UriArm64": "https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip", "Labels": [ + "Stable", "Latest" ] } @@ -69,9 +62,9 @@ ] }, { - "Version": "6.1.2.42921", - "UriX64": "https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/digitalwatchdog/42921/dwspectrum-server_update-6.1.2.42921-linux_arm64.zip", + "Version": "6.1.2.42997", + "UriX64": "https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip", "Labels": [ "Latest" ] From 1214ae72c952c241c6e203ca11846af34ca3a1ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:16:51 +0000 Subject: [PATCH 05/12] Bump the nuget-deps group with 2 updates (#431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated [Microsoft.Extensions.Http.Resilience](https://github.com/dotnet/extensions) from 10.6.0 to 10.7.0.
Release notes _Sourced from [Microsoft.Extensions.Http.Resilience's releases](https://github.com/dotnet/extensions/releases)._ ## 10.7.0 v10.7.0 graduates the [Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes) package to stable. The package registers a Kubernetes-aware `ResourceQuotaProvider` that reads the pod's CPU and memory requests and limits and exposes them to `Microsoft.Extensions.Diagnostics.ResourceMonitoring` as baseline and maximum quotas, which then feed the request and limit dimensions of the published resource utilization metrics. The companion `ResourceQuota` and `ResourceQuotaProvider` types in `Microsoft.Extensions.Diagnostics.ResourceMonitoring` graduate to stable in the same change so that consumers can implement custom quota providers without taking an experimental dependency. On the AI side, `Microsoft.Extensions.AI.OpenAI` moves to OpenAI 2.11.0 and fixes a deserialization bug in `ToolJson.AdditionalProperties` so that JSON Schema `additionalProperties` values shaped as sub-schema objects (for example `{"type":"string"}`) are preserved instead of throwing during deserialization. `HostedFileContent.SizeInBytes` and `HostedFileContent.CreatedAt` graduate to stable since both values are consistently available across hosted-file providers, while `Purpose` and `Scope` remain experimental as provider-shaped vocabulary. `FunctionInvokingChatClient` drops a backward-compat path that auto-marked `ToolApprovalResponseContent` entries with `InformationalOnly: true`; consumers that need to continue accepting sessions serialized before #​7468 can use the sample `ApprovalHistoryNormalizingChatClient` middleware added in the test project. ## Experimental API Changes ### Now Stable * `Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes` package is now stable #​7253 * Resource Monitoring `ResourceQuota` and `ResourceQuotaProvider` APIs are now stable (previously `EXTEXP0008`) #​7253 * `HostedFileContent.SizeInBytes` and `HostedFileContent.CreatedAt` are now stable (previously `MEAI001`) #​7513 ## What's Changed ### AI * Graduate HostedFileContent.SizeInBytes and HostedFileContent.CreatedAt #​7513 by @​jozkee (co-authored by @​Copilot) * Remove backward-compat InformationalOnly case from FICC; suggest middleware workaround #​7538 by @​jozkee (co-authored by @​Copilot) * Upgrade OpenAI package from 2.10.0 to 2.11.0 #​7544 by @​jozkee (co-authored by @​Copilot) * Fix ToolJson.AdditionalProperties to accept sub-schema objects #​7546 by @​jozkee (co-authored by @​Copilot) ### Diagnostics, Health Checks, and Resource Monitoring * Move Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes to stable #​7253 by @​amadeuszl (co-authored by @​Copilot) ## Repository Infrastructure Updates * [main] Update dependencies from dotnet/arcade #​7521 * Bump dotnet-reportgenerator-globaltool from 5.5.9 to 5.5.10 #​7522 * Bump dotnet-coverage from 18.6.2 to 18.7.0 #​7530 * Bump PowerShell from 7.6.1 to 7.6.2 #​7531 * Bump qs from 6.15.1 to 6.15.2 in /src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript #​7532 * [main] Update dependencies from dotnet/arcade #​7534 * Bump tmp from 0.2.5 to 0.2.6 in /src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript #​7537 ## Acknowledgements * @​ericstj submitted issue #​7509 (resolved by #​7544) * @​scottt732 submitted issue #​7540 (resolved by #​7546) * @​DeagleGross @​wtgodbe @​dariusclay @​evgenyfedorov2 @​peterwald @​PranavSenthilnathan @​shyamnamboodiripad @​stephentoub @​tarekgh reviewed pull requests **Full Changelog**: https://github.com/dotnet/extensions/compare/v10.6.0...v10.7.0 Commits viewable in [compare view](https://github.com/dotnet/extensions/compare/v10.6.0...v10.7.0).
Updated [System.CommandLine](https://github.com/dotnet/dotnet) from 2.0.8 to 2.0.9.
Release notes _Sourced from [System.CommandLine's releases](https://github.com/dotnet/dotnet/releases)._ No release notes found for this version range. Commits viewable in [compare view](https://github.com/dotnet/dotnet/commits).
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- CreateMatrix/CreateMatrix.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CreateMatrix/CreateMatrix.csproj b/CreateMatrix/CreateMatrix.csproj index 302684e..0d7c3e4 100644 --- a/CreateMatrix/CreateMatrix.csproj +++ b/CreateMatrix/CreateMatrix.csproj @@ -16,11 +16,11 @@ true - + - + From da8197d00d3b13e184d2b70e2db458d2daf0b1ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:14:36 +0000 Subject: [PATCH 06/12] Bump the nuget-deps group with 1 update (#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated [dotnet-outdated-tool](https://github.com/dotnet-outdated/dotnet-outdated) from 4.8.0 to 4.8.1.
Release notes _Sourced from [dotnet-outdated-tool's releases](https://github.com/dotnet-outdated/dotnet-outdated/releases)._ ## 4.8.1 ## What's Changed * Add `#:sdk` support for .NET file-based apps by @​devlead in https://github.com/dotnet-outdated/dotnet-outdated/pull/755 ## New Contributors * @​devlead made their first contribution in https://github.com/dotnet-outdated/dotnet-outdated/pull/755 **Full Changelog**: https://github.com/dotnet-outdated/dotnet-outdated/compare/v4.8.0...v4.8.1 Commits viewable in [compare view](https://github.com/dotnet-outdated/dotnet-outdated/compare/v4.8.0...v4.8.1).
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dotnet-outdated-tool&package-manager=nuget&previous-version=4.8.0&new-version=4.8.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index efeff64..e747974 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "dotnet-outdated-tool": { - "version": "4.8.0", + "version": "4.8.1", "commands": [ "dotnet-outdated" ], From 197a699fae1d7e21a168bf0385beaba013da903f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:14:56 +0000 Subject: [PATCH 07/12] Build(deps): Bump actions/checkout from 6 to 7 in the actions-deps group (#435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-deps group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7
Release notes

Sourced from actions/checkout's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0

v6.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3

v6.0.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6.0.1...v6.0.2

v6.0.1

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-base-images-task.yml | 2 +- .github/workflows/build-docker-task.yml | 4 ++-- .github/workflows/get-version-task.yml | 2 +- .github/workflows/publish-docker-readme-task.yml | 4 ++-- .github/workflows/publish-release.yml | 2 +- .github/workflows/run-codegen-pull-request-task.yml | 2 +- .github/workflows/test-pull-request.yml | 2 +- .github/workflows/test-release-task.yml | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-base-images-task.yml b/.github/workflows/build-base-images-task.yml index 847288f..67287b7 100644 --- a/.github/workflows/build-base-images-task.yml +++ b/.github/workflows/build-base-images-task.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index 6413f5f..478c134 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} @@ -120,7 +120,7 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 20d710a..ad4a83d 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -42,7 +42,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 ref: ${{ inputs.ref || github.ref }} diff --git a/.github/workflows/publish-docker-readme-task.yml b/.github/workflows/publish-docker-readme-task.yml index e970950..6bd992f 100644 --- a/.github/workflows/publish-docker-readme-task.yml +++ b/.github/workflows/publish-docker-readme-task.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 22d8b3e..53f39f2 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -89,7 +89,7 @@ jobs: # so the uploaded release files come from the same commit the tag points # at even if `main` advances mid-run. - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ needs.get-version.outputs.GitCommitId }} diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 4e01f79..364abee 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -55,7 +55,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ matrix.target.ref }} token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 8c6fec9..260f588 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -25,7 +25,7 @@ jobs: # (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 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml index dfa5278..8b2fe5e 100644 --- a/.github/workflows/test-release-task.yml +++ b/.github/workflows/test-release-task.yml @@ -18,7 +18,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Check code style step run: | From 561fc06f0cdc6a919f8de9dab7d5518da1d31345 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 19 Jun 2026 10:23:20 -0700 Subject: [PATCH 08/12] Fix version-forward regression creating duplicate versions (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The daily **CodeGen** action started failing with **exit code 134 (SIGABRT)** — a failed `Debug.Assert(versionSet.Count == productInfo.Versions.Count)` in `ImageInfo.CreateImages`. Reproduced from the WisenetWAVE trace: the online *Latest* regressed below a version already present as *Stable*, and `ReleaseVersionForward` restored the old *Latest* by **adding a second row** for `6.1.2.42921`, so two entries shared a version number and collapsed in the sorted set. The vendor JSON was valid — our anti-regression merge created the duplicate. ## Fix - **`ReleaseVersionForward`**: fold the restored label into the existing same-version entry instead of adding a duplicate row → `6.1.2[Stable, Latest]`. - **`ProductInfo.VerifyNoDuplicateVersions()`**: an all-build guard that throws (with product + versions) before the version file is written, covering both the fetch and forward-merge paths. The `Debug.Assert` stays as a backstop. - **Defensive vendor parsing** (`ReleasesJsonSchema.VerifyReleases`): reject `releases.json` that tags one version number with conflicting publication types; fold benign same-type duplicates. ## Hardening / cleanup - Consolidated version parsing into `VersionInfo.NormalizeVersion`/`ParseVersion`, and the version/label assembly into `ProductInfo.CreateVersionInfo` (shared with the test mirror, removing the divergence-prone duplication). - Made failure paths descriptive: replaced context-free `Debug.Assert`/bare exceptions on the vendor-data paths with messages carrying product, build number, version, and URL; disambiguated "label not found" warnings. ## Tests - WisenetWAVE regression (folds to one `6.1.2` entry with both labels, no duplicate numbers). - `VerifyNoDuplicateVersions` throws on duplicates. - Invalid vendor JSON: throws on conflicting publication types, folds same-type duplicates. All 20 tests pass; CSharpier + `dotnet format style` clean. ## Release chore (maintainer-prepared) Regenerated `Docker/*` and `Make/{Version,Matrix}.json`, release notes in `README.md` / `HISTORY.md` (Version 2.13), and `version.json` floor bump to 2.14 (keeps develop a minor ahead). --------- Co-authored-by: Claude Opus 4.8 (1M context) --- CreateMatrix/Dockerfile.cs | 12 ++- CreateMatrix/ImageInfo.cs | 3 +- CreateMatrix/PackagesJsonSchema.cs | 5 +- CreateMatrix/ProductInfo.cs | 116 ++++++++++++++++++----- CreateMatrix/Program.cs | 3 + CreateMatrix/ReleaseVersionForward.cs | 63 ++++++++++-- CreateMatrix/ReleasesJsonSchema.cs | 61 +++++++++++- CreateMatrix/VersionInfo.cs | 24 +++-- CreateMatrix/VersionJsonSchema.cs | 4 +- CreateMatrixTests/ReleasesTests.cs | 91 ++++++++++++++++-- CreateMatrixTests/VersionForwardTests.cs | 91 ++++++++++++++++++ Docker/DWSpectrum-LSIO.Dockerfile | 8 +- Docker/DWSpectrum.Dockerfile | 8 +- Docker/NxGo-LSIO.Dockerfile | 8 +- Docker/NxGo.Dockerfile | 8 +- Docker/NxMeta-LSIO.Dockerfile | 8 +- Docker/NxMeta.Dockerfile | 8 +- Docker/NxWitness-LSIO.Dockerfile | 8 +- Docker/NxWitness.Dockerfile | 8 +- HISTORY.md | 2 + Make/Matrix.json | 52 +++++----- Make/Version.json | 10 +- README.md | 65 +++++++------ version.json | 2 +- 24 files changed, 511 insertions(+), 157 deletions(-) diff --git a/CreateMatrix/Dockerfile.cs b/CreateMatrix/Dockerfile.cs index 57a23a6..f6905d7 100644 --- a/CreateMatrix/Dockerfile.cs +++ b/CreateMatrix/Dockerfile.cs @@ -18,8 +18,9 @@ VersionInfo.LabelType label foreach (ProductInfo.ProductType productType in ProductInfo.GetProductTypes()) { // Find the matching product - ProductInfo? productInfo = productList.Find(item => item.Product == productType); - ArgumentNullException.ThrowIfNull(productInfo); + ProductInfo productInfo = + productList.Find(item => item.Product == productType) + ?? throw new InvalidOperationException($"Product not found: {productType}"); // Get the version for the label, not all releases include Beta and RC labels VersionInfo? versionInfo = productInfo.Versions.Find(item => @@ -38,7 +39,12 @@ VersionInfo.LabelType label item.Labels.Contains(VersionInfo.LabelType.Latest) ); } - ArgumentNullException.ThrowIfNull(versionInfo); + if (versionInfo == null) + { + throw new InvalidOperationException( + $"{productType}: No version found for label {label} or Latest" + ); + } // Create the standard Docker file string dockerFile = CreateDockerfile(productType, versionInfo, false); diff --git a/CreateMatrix/ImageInfo.cs b/CreateMatrix/ImageInfo.cs index 7370709..1a533b3 100644 --- a/CreateMatrix/ImageInfo.cs +++ b/CreateMatrix/ImageInfo.cs @@ -39,7 +39,8 @@ private ImageInfo(ProductInfo.ProductType productType, BranchType branchType, Ba Product = productType; Branch = branchType; Base = baseType; - Name = baseType == BaseType.Ubuntu ? productType.ToString() : $"{productType}-LSIO"; + // The image name matches the Dockerfile name + Name = ProductInfo.GetDocker(productType, baseType == BaseType.LSIO); } private void AddArgs(VersionInfo versionInfo) diff --git a/CreateMatrix/PackagesJsonSchema.cs b/CreateMatrix/PackagesJsonSchema.cs index ed8ef5a..ad59f1a 100644 --- a/CreateMatrix/PackagesJsonSchema.cs +++ b/CreateMatrix/PackagesJsonSchema.cs @@ -79,7 +79,10 @@ CancellationToken cancellationToken // Deserialize JSON PackagesJsonSchema packagesSchema = FromJson(jsonString); ArgumentNullException.ThrowIfNull(packagesSchema); - ArgumentOutOfRangeException.ThrowIfZero(packagesSchema.Packages.Count); + if (packagesSchema.Packages.Count == 0) + { + throw new InvalidOperationException($"No packages found in {packagesUri}"); + } // Return packages return packagesSchema.Packages; diff --git a/CreateMatrix/ProductInfo.cs b/CreateMatrix/ProductInfo.cs index 76faa17..510ef80 100644 --- a/CreateMatrix/ProductInfo.cs +++ b/CreateMatrix/ProductInfo.cs @@ -33,8 +33,14 @@ public static string GetRelease(ProductType productType) => ProductType.NxWitness => "default", ProductType.DWSpectrum => "digitalwatchdog", ProductType.WisenetWAVE => "hanwha", - ProductType.None => throw new NotImplementedException(), - _ => throw new InvalidEnumArgumentException(nameof(Product)), + ProductType.None => throw new InvalidOperationException( + $"{nameof(ProductType)} is None" + ), + _ => throw new InvalidEnumArgumentException( + nameof(productType), + (int)productType, + typeof(ProductType) + ), }; // Used for ${COMPANY_NAME} mediaserver install path and user account @@ -46,8 +52,14 @@ public static string GetCompany(ProductType productType) => ProductType.NxWitness => "networkoptix", ProductType.DWSpectrum => "digitalwatchdog", ProductType.WisenetWAVE => "hanwha", - ProductType.None => throw new NotImplementedException(), - _ => throw new InvalidEnumArgumentException(nameof(Product)), + ProductType.None => throw new InvalidOperationException( + $"{nameof(ProductType)} is None" + ), + _ => throw new InvalidEnumArgumentException( + nameof(productType), + (int)productType, + typeof(ProductType) + ), }; // Used for ${LABEL_DESCRIPTION} in Dockerfile @@ -59,12 +71,20 @@ public static string GetDescription(ProductType productType) => ProductType.NxWitness => "Nx Witness VMS", ProductType.DWSpectrum => "DW Spectrum IPVMS", ProductType.WisenetWAVE => "Wisenet WAVE VMS", - ProductType.None => throw new NotImplementedException(), - _ => throw new InvalidEnumArgumentException(nameof(Product)), + ProductType.None => throw new InvalidOperationException( + $"{nameof(ProductType)} is None" + ), + _ => throw new InvalidEnumArgumentException( + nameof(productType), + (int)productType, + typeof(ProductType) + ), }; - // Dockerfile name, excluding the .Dockerfile extension - // TODO: Consolidate with ImageInfo.SetName(), e.g. add enum for Ubuntu, LSIO, etc. + // Dockerfile name, excluding the .Dockerfile extension. + // This is the single source of the image/Dockerfile naming convention; ImageInfo derives its + // name from here. The base variant is a bool (Ubuntu vs LSIO); promote it to an enum only if a + // third base type is ever added (which would also ripple through ComposeFile/DockerFile). public static string GetDocker(ProductType productType, bool lsio) => $"{productType}{(lsio ? "-LSIO" : "")}"; @@ -81,9 +101,6 @@ .. from ProductType productType in GetProductTypes() public async Task FetchVersionsAsync(CancellationToken cancellationToken) { - // Match the logic with ReleasesTests.CreateProductInfo() - // TODO: Refactor to reduce duplication and chance of divergence - // Get version information using releases.json and package.json Log.Logger.Information("{Product}: Getting online release information...", Product); try @@ -105,13 +122,8 @@ public async Task FetchVersionsAsync(CancellationToken cancellationToken) continue; } - // Set version - VersionInfo versionInfo = new(); - Debug.Assert(!string.IsNullOrEmpty(release.Version)); - versionInfo.SetVersion(release.Version); - - // Add the label - AddLabel(versionInfo, release.GetLabel()); + // Set the version and label + VersionInfo versionInfo = CreateVersionInfo(release); // Get the build number from the version int buildNumber = versionInfo.GetBuildNumber(); @@ -122,12 +134,28 @@ public async Task FetchVersionsAsync(CancellationToken cancellationToken) .ConfigureAwait(false); // Get the x64 and arm64 server ubuntu server packages - Package? packageX64 = packageList.Find(item => item.IsX64Server()); - Debug.Assert(packageX64 != null); - Debug.Assert(!string.IsNullOrEmpty(packageX64.File)); - Package? packageArm64 = packageList.Find(item => item.IsArm64Server()); - Debug.Assert(packageArm64 != null); - Debug.Assert(!string.IsNullOrEmpty(packageArm64.File)); + Package packageX64 = + packageList.Find(item => item.IsX64Server()) + ?? throw new InvalidOperationException( + $"{Product}: No x64 Ubuntu server package found for build {buildNumber} (version {versionInfo.Version})" + ); + if (string.IsNullOrEmpty(packageX64.File)) + { + throw new InvalidOperationException( + $"{Product}: x64 Ubuntu server package for build {buildNumber} (version {versionInfo.Version}) has no file name" + ); + } + Package packageArm64 = + packageList.Find(item => item.IsArm64Server()) + ?? throw new InvalidOperationException( + $"{Product}: No arm64 Ubuntu server package found for build {buildNumber} (version {versionInfo.Version})" + ); + if (string.IsNullOrEmpty(packageArm64.File)) + { + throw new InvalidOperationException( + $"{Product}: arm64 Ubuntu server package for build {buildNumber} (version {versionInfo.Version}) has no file name" + ); + } // Create the download URLs // https://updates.networkoptix.com/{product}/{build}/{file} @@ -153,6 +181,22 @@ public async Task FetchVersionsAsync(CancellationToken cancellationToken) } } + public VersionInfo CreateVersionInfo(Release release) + { + // Create a version with its label from the release. + // Note: AddLabel() may move the label off other versions already in the list. + if (string.IsNullOrEmpty(release.Version)) + { + throw new InvalidOperationException( + $"{Product}: Release has no version (publication type '{release.PublicationType}')" + ); + } + VersionInfo versionInfo = new(); + versionInfo.SetVersion(release.Version); + AddLabel(versionInfo, release.GetLabel()); + return versionInfo; + } + private bool VerifyVersion(VersionInfo versionInfo) { // Static rules: @@ -285,6 +329,30 @@ public void VerifyLabels() Versions.Count(item => item.Labels.Contains(VersionInfo.LabelType.RC)), 1 ); + + // Must have no duplicate version numbers + VerifyNoDuplicateVersions(); + } + + public void VerifyNoDuplicateVersions() + { + // Each version number must appear at most once. + // Duplicates collapse when the matrix builds a set keyed by version number + // (see ImageInfo.CreateImages() and VersionInfoComparer) and must be rejected + // rather than written to the version file. + List duplicateVersions = + [ + .. Versions + .GroupBy(item => VersionInfo.ParseVersion(item.Version)) + .Where(group => group.Count() > 1) + .Select(group => group.First().Version), + ]; + if (duplicateVersions.Count > 0) + { + throw new InvalidOperationException( + $"{Product}: Duplicate version numbers found: {string.Join(", ", duplicateVersions)}" + ); + } } public void LogInformation() diff --git a/CreateMatrix/Program.cs b/CreateMatrix/Program.cs index d7e3f81..770f72b 100644 --- a/CreateMatrix/Program.cs +++ b/CreateMatrix/Program.cs @@ -114,6 +114,9 @@ internal async Task ExecuteMatrixAsync() // Make sure the labelled version numbers do not regress ReleaseVersionForward.Verify(fileSchema.Products, onlineSchema.Products); + // Make sure the forward merge did not introduce duplicate version numbers + onlineSchema.Products.ForEach(productInfo => productInfo.VerifyNoDuplicateVersions()); + // Verify URL's foreach (ProductInfo productInfo in onlineSchema.Products) { diff --git a/CreateMatrix/ReleaseVersionForward.cs b/CreateMatrix/ReleaseVersionForward.cs index 780f43c..debfd37 100644 --- a/CreateMatrix/ReleaseVersionForward.cs +++ b/CreateMatrix/ReleaseVersionForward.cs @@ -28,13 +28,21 @@ private static void Verify( VersionInfo.LabelType label ) { - // TODO: It is possible that a label is released, then pulled, then re-released with a lesser version + // NOTE: Forward-only is intentional; a published tag must not regress to a lesser version. + // If a label is released, then pulled, then re-released with a lesser version, we keep the + // old (higher) version. This is harmless while the old build is still downloadable, and if + // its files were actually removed the run fails loudly in VerifyUrlsAsync (404) for a human + // to resolve, e.g. by manually adjusting Version.json. There is no silent-corruption path. // Find label in old and new product, skip if not present VersionInfo? oldVersion = oldProduct.Versions.Find(item => item.Labels.Contains(label)); if (oldVersion == null) { - Log.Logger.Warning("{Product}:{Label} : Label not found", oldProduct.Product, label); + Log.Logger.Warning( + "{Product}:{Label} : Label not found in old versions", + oldProduct.Product, + label + ); return; } @@ -42,7 +50,11 @@ VersionInfo.LabelType label VersionInfo? newVersion = newProduct.Versions.Find(item => item.Labels.Contains(label)); if (newVersion == null) { - Log.Logger.Warning("{Product}:{Label} : Label not found", newProduct.Product, label); + Log.Logger.Warning( + "{Product}:{Label} : Label not found in new versions", + newProduct.Product, + label + ); return; } @@ -71,16 +83,51 @@ VersionInfo.LabelType label newVersion.Version ); - // Replace the new version with the old version + // Remove the regressed new version _ = newProduct.Versions.Remove(newVersion); - newProduct.Versions.Add(oldVersion); + + // The old version number may already be present in the new list under a different + // label (e.g. restoring Latest onto a version that is already Stable). Fold the + // old version's labels into that existing entry instead of adding a duplicate row, + // otherwise two entries would share a version number. + VersionInfo? existingVersion = newProduct.Versions.Find(item => + item.CompareTo(oldVersion) == 0 + ); + if (existingVersion == null) + { + // No existing entry, add the old version + newProduct.Versions.Add(oldVersion); + } + else + { + // Fold the old version's labels into the existing entry + Log.Logger.Warning( + "{Product}:{Label} Folding OldVersion: {OldVersion} labels into existing version", + newProduct.Product, + label, + oldVersion.Version + ); + foreach (VersionInfo.LabelType oldLabel in oldVersion.Labels) + { + if (!existingVersion.Labels.Contains(oldLabel)) + { + existingVersion.Labels.Add(oldLabel); + } + } + existingVersion.Labels.Sort(); + } } else { - // TODO: Unwind labels to replace only specific version-label pairs + // The label moved between versions that carry different label sets, so a surgical + // per-version-label swap is ambiguous. Rather than attempting to unwind individual + // version-label pairs, take the conservative approach and revert the whole product to + // the last-known-good versions. This may discard newer online versions until the + // regression clears, but it avoids producing an inconsistent label arrangement. Log.Logger.Warning( - "{Product}: Using old versions instead of new versions", - newProduct.Product + "{Product}:{Label} : Labels differ, reverting all versions to old versions", + newProduct.Product, + label ); // Replace all versions if the labels do not match diff --git a/CreateMatrix/ReleasesJsonSchema.cs b/CreateMatrix/ReleasesJsonSchema.cs index 44af624..d44909c 100644 --- a/CreateMatrix/ReleasesJsonSchema.cs +++ b/CreateMatrix/ReleasesJsonSchema.cs @@ -33,8 +33,8 @@ public VersionInfo.LabelType GetLabel() => : VersionInfo.LabelType.Latest, RcPublication => VersionInfo.LabelType.RC, BetaPublication => VersionInfo.LabelType.Beta, - _ => throw new InvalidEnumArgumentException( - $"Unknown PublicationType: {PublicationType}" + _ => throw new InvalidOperationException( + $"Unknown publication type '{PublicationType}' for version {Version}" ), }; @@ -86,10 +86,61 @@ CancellationToken cancellationToken // Deserialize JSON ReleasesJsonSchema releasesSchema = FromJson(jsonString); ArgumentNullException.ThrowIfNull(releasesSchema); - ArgumentOutOfRangeException.ThrowIfZero(releasesSchema.Releases.Count); + if (releasesSchema.Releases.Count == 0) + { + throw new InvalidOperationException($"No releases found in {releasesUri}"); + } + + // Verify the vendor data and return the folded releases + return VerifyReleases(releasesSchema.Releases); + } - // Return releases - return releasesSchema.Releases; + public static List VerifyReleases(List releases) + { + // A version number must carry a single, unambiguous publication type. + // Fold benign duplicates (same version and publication type), reject conflicts. + ArgumentNullException.ThrowIfNull(releases); + + // A version number must be present and parseable, otherwise grouping below would + // throw a context-free FormatException from Version parsing. + foreach (Release release in releases) + { + if ( + string.IsNullOrEmpty(release.Version) + || !Version.TryParse(VersionInfo.NormalizeVersion(release.Version), out _) + ) + { + throw new InvalidOperationException( + $"{release.Product}: Invalid or missing version '{release.Version}' (publication type '{release.PublicationType}')" + ); + } + } + + List verifiedReleases = []; + foreach ( + IGrouping<(string Product, Version Version), Release> group in releases.GroupBy( + release => (release.Product, VersionInfo.ParseVersion(release.Version)) + ) + ) + { + // A version must not be tagged with more than one publication type + List publicationTypes = + [ + .. group + .Select(release => release.PublicationType) + .Distinct(StringComparer.OrdinalIgnoreCase), + ]; + if (publicationTypes.Count > 1) + { + throw new InvalidOperationException( + $"{group.Key.Product}: Version {group.First().Version} has conflicting publication types: {string.Join(", ", publicationTypes)}" + ); + } + + // Keep the first entry, folding any same-version same-type duplicates + verifiedReleases.Add(group.First()); + } + return verifiedReleases; } } diff --git a/CreateMatrix/VersionInfo.cs b/CreateMatrix/VersionInfo.cs index cebb237..ada52c1 100644 --- a/CreateMatrix/VersionInfo.cs +++ b/CreateMatrix/VersionInfo.cs @@ -20,27 +20,31 @@ public int GetBuildNumber() => // Extract the build number using the Version class (vs. regex) // 5.0.0.35271 -> 35271 // 5.1.0.35151 R1 -> 35151 - new Version(Version).Revision; + ParseVersion(Version).Revision; - public void SetVersion(string version) + public void SetVersion(string version) => + // Store the normalized version number + Version = NormalizeVersion(version); + + public static string NormalizeVersion(string version) { - // Remove Rxx from version string + // Remove the " Rxx" suffix from the version string // "5.0.0.35134 R10" -> "5.0.0.35134" int spaceIndex = version.IndexOf(' ', StringComparison.Ordinal); - Version = spaceIndex == -1 ? version : version[..spaceIndex]; + return spaceIndex == -1 ? version : version[..spaceIndex]; } + public static Version ParseVersion(string version) => + // Parse the version number using the Version class, ignoring any " Rxx" suffix + new(NormalizeVersion(version)); + public int CompareTo(VersionInfo rhs) => Compare(this, rhs); public int CompareTo(string rhs) => Compare(Version, rhs); - public static int Compare(string lhs, string rhs) - { + public static int Compare(string lhs, string rhs) => // Compare version numbers using Version class - Version lhsVersion = new(lhs); - Version rhsVersion = new(rhs); - return lhsVersion.CompareTo(rhsVersion); - } + ParseVersion(lhs).CompareTo(ParseVersion(rhs)); public static int Compare(VersionInfo lhs, VersionInfo rhs) => Compare(lhs.Version, rhs.Version); diff --git a/CreateMatrix/VersionJsonSchema.cs b/CreateMatrix/VersionJsonSchema.cs index 42e2d32..168b2d0 100644 --- a/CreateMatrix/VersionJsonSchema.cs +++ b/CreateMatrix/VersionJsonSchema.cs @@ -49,7 +49,9 @@ private static VersionJsonSchema FromJson(string json) // Breaking change, UriArm64 is required in ARM64 docker builds // Unknown version default: - throw new NotImplementedException(); + throw new NotSupportedException( + $"Unsupported schema version: {schemaVersion} (expected {Version})" + ); } } } diff --git a/CreateMatrixTests/ReleasesTests.cs b/CreateMatrixTests/ReleasesTests.cs index e06ca4a..0e6e9ec 100644 --- a/CreateMatrixTests/ReleasesTests.cs +++ b/CreateMatrixTests/ReleasesTests.cs @@ -226,17 +226,94 @@ public void MultipleReleases() version.Version.Should().Be("4.0"); } + [Fact] + public void ConflictingPublicationTypes_Throws() + { + // The same version number tagged with two different publication types is contradictory + // vendor data and must be rejected rather than folded into our data. + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "6.1.2.42921", + }, + new Release { PublicationType = Release.BetaPublication, Version = "6.1.2.42921" }, + }, + }; + + Action act = () => CreateProductInfo(releasesSchema); + act.Should().Throw(); + } + + [Fact] + public void DuplicatePublicationType_Folds() + { + // The same version number listed twice with the same publication type is a benign + // duplicate and is folded to a single entry. + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "6.1.2.42921", + }, + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "6.1.2.42921", + }, + }, + }; + + ProductInfo productInfo = CreateProductInfo(releasesSchema); + + // Folded to a single version + productInfo.Versions.Should().ContainSingle(); + productInfo.Versions.First().Version.Should().Be("6.1.2.42921"); + } + + [Fact] + public void MissingVersion_Throws() + { + // A release with a missing or unparseable version must be rejected with a clear + // error rather than a context-free version-parsing failure. + ReleasesJsonSchema releasesSchema = new() + { + Releases = + { + new Release + { + PublicationType = Release.ReleasePublication, + ReleaseDate = 1, + ReleaseDeliveryDays = 1, + Version = "", + }, + }, + }; + + Action act = () => CreateProductInfo(releasesSchema); + act.Should().Throw(); + } + private static ProductInfo CreateProductInfo(ReleasesJsonSchema releasesSchema) { - // Match the logic with ProductInfo.GetVersions() - // TODO: Refactor to reduce duplication and chance of divergence + // Mirror of the non-network portion of ProductInfo.FetchVersionsAsync(), + // sharing the version and label logic via ProductInfo.CreateVersionInfo(). ProductInfo productInfo = new(); - foreach (Release release in releasesSchema.Releases) + foreach (Release release in ReleasesJsonSchema.VerifyReleases(releasesSchema.Releases)) { - VersionInfo versionInfo = new(); - versionInfo.SetVersion(release.Version); - productInfo.AddLabel(versionInfo, release.GetLabel()); - productInfo.Versions.Add(versionInfo); + productInfo.Versions.Add(productInfo.CreateVersionInfo(release)); } productInfo.VerifyLabels(); diff --git a/CreateMatrixTests/VersionForwardTests.cs b/CreateMatrixTests/VersionForwardTests.cs index 0b35bb1..cc8e8f8 100644 --- a/CreateMatrixTests/VersionForwardTests.cs +++ b/CreateMatrixTests/VersionForwardTests.cs @@ -175,4 +175,95 @@ public void VersionRegress() ?.Version; betaVersion.Should().Be("4.0"); } + + [Fact] + public void VersionRegress_FoldsOntoExistingVersion() + { + // Reproduces the WisenetWAVE codegen failure: the online "Latest" regressed below a + // version that is already present as "Stable", so restoring the old "Latest" must fold + // onto the existing entry instead of adding a duplicate version row. + List oldProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.WisenetWAVE, + Versions = + { + new VersionInfo + { + Version = "6.0.5.41290", + Labels = { VersionInfo.LabelType.Stable }, + }, + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Latest }, + }, + }, + }, + ]; + List newProductList = + [ + new ProductInfo + { + Product = ProductInfo.ProductType.WisenetWAVE, + Versions = + { + new VersionInfo + { + Version = "6.1.1.42624", + Labels = { VersionInfo.LabelType.Latest }, + }, + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Stable }, + }, + }, + }, + ]; + + ReleaseVersionForward.Verify(oldProductList, newProductList); + ProductInfo productInfo = newProductList.First(); + + // Folded to a single 6.1.2.42921 entry carrying both Stable and Latest + productInfo.Versions.Should().ContainSingle(); + VersionInfo version = productInfo.Versions.First(); + version.Version.Should().Be("6.1.2.42921"); + version + .Labels.Should() + .BeEquivalentTo([VersionInfo.LabelType.Stable, VersionInfo.LabelType.Latest]); + + // No duplicate version numbers + productInfo.Versions.Select(item => item.Version).Should().OnlyHaveUniqueItems(); + productInfo.Invoking(item => item.VerifyNoDuplicateVersions()).Should().NotThrow(); + } + + [Fact] + public void VerifyNoDuplicateVersions_Throws() + { + // Two entries sharing a version number must be rejected + ProductInfo productInfo = new() + { + Product = ProductInfo.ProductType.NxMeta, + Versions = + { + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Stable }, + }, + new VersionInfo + { + Version = "6.1.2.42921", + Labels = { VersionInfo.LabelType.Latest }, + }, + }, + }; + + productInfo + .Invoking(item => item.VerifyNoDuplicateVersions()) + .Should() + .Throw(); + } } diff --git a/Docker/DWSpectrum-LSIO.Dockerfile b/Docker/DWSpectrum-LSIO.Dockerfile index 6fe4e61..8b03d57 100644 --- a/Docker/DWSpectrum-LSIO.Dockerfile +++ b/Docker/DWSpectrum-LSIO.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base-lsio:ubuntu-noble # Labels ARG LABEL_NAME="DWSpectrum-LSIO" ARG LABEL_DESCRIPTION="DW Spectrum IPVMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.2.42997" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.2.42997" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="digitalwatchdog" diff --git a/Docker/DWSpectrum.Dockerfile b/Docker/DWSpectrum.Dockerfile index 52d66c8..b7ae9f9 100644 --- a/Docker/DWSpectrum.Dockerfile +++ b/Docker/DWSpectrum.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base:ubuntu-noble # Labels ARG LABEL_NAME="DWSpectrum" ARG LABEL_DESCRIPTION="DW Spectrum IPVMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.2.42997" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/digitalwatchdog/42624/dwspectrum-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/digitalwatchdog/42997/dwspectrum-server_update-6.1.2.42997-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.2.42997" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="digitalwatchdog" diff --git a/Docker/NxGo-LSIO.Dockerfile b/Docker/NxGo-LSIO.Dockerfile index 7d4bd0a..8ddb28d 100644 --- a/Docker/NxGo-LSIO.Dockerfile +++ b/Docker/NxGo-LSIO.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base-lsio:ubuntu-noble # Labels ARG LABEL_NAME="NxGo-LSIO" ARG LABEL_DESCRIPTION="Nx Go VMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.2.42921" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.2.42921" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="networkoptix" diff --git a/Docker/NxGo.Dockerfile b/Docker/NxGo.Dockerfile index c77be6a..e717745 100644 --- a/Docker/NxGo.Dockerfile +++ b/Docker/NxGo.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base:ubuntu-noble # Labels ARG LABEL_NAME="NxGo" ARG LABEL_DESCRIPTION="Nx Go VMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.2.42921" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/nxgo/42624/nxgo-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/nxgo/42921/nxgo-server_update-6.1.2.42921-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.2.42921" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="networkoptix" diff --git a/Docker/NxMeta-LSIO.Dockerfile b/Docker/NxMeta-LSIO.Dockerfile index 2778c7a..e059582 100644 --- a/Docker/NxMeta-LSIO.Dockerfile +++ b/Docker/NxMeta-LSIO.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base-lsio:ubuntu-noble # Labels ARG LABEL_NAME="NxMeta-LSIO" ARG LABEL_DESCRIPTION="Nx Meta VMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.1.42649" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.1.42649" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="networkoptix-metavms" diff --git a/Docker/NxMeta.Dockerfile b/Docker/NxMeta.Dockerfile index d827fa6..0b1e1d8 100644 --- a/Docker/NxMeta.Dockerfile +++ b/Docker/NxMeta.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base:ubuntu-noble # Labels ARG LABEL_NAME="NxMeta" ARG LABEL_DESCRIPTION="Nx Meta VMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.1.42649" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/metavms/42624/metavms-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/metavms/42649/metavms-server_update-6.1.1.42649-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.1.42649" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="networkoptix-metavms" diff --git a/Docker/NxWitness-LSIO.Dockerfile b/Docker/NxWitness-LSIO.Dockerfile index 62e32f0..3638279 100644 --- a/Docker/NxWitness-LSIO.Dockerfile +++ b/Docker/NxWitness-LSIO.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base-lsio:ubuntu-noble # Labels ARG LABEL_NAME="NxWitness-LSIO" ARG LABEL_DESCRIPTION="Nx Witness VMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.2.42921" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.2.42921" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="networkoptix" diff --git a/Docker/NxWitness.Dockerfile b/Docker/NxWitness.Dockerfile index f46374b..5874723 100644 --- a/Docker/NxWitness.Dockerfile +++ b/Docker/NxWitness.Dockerfile @@ -13,13 +13,13 @@ FROM docker.io/ptr727/nx-base:ubuntu-noble # Labels ARG LABEL_NAME="NxWitness" ARG LABEL_DESCRIPTION="Nx Witness VMS" -ARG LABEL_VERSION="6.1.1.42624" +ARG LABEL_VERSION="6.1.2.42921" # Download URL and version # Current values are defined by the build pipeline -ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_x64.zip" -ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/default/42624/nxwitness-server_update-6.1.1.42624-linux_arm64.zip" -ARG DOWNLOAD_VERSION="6.1.1.42624" +ARG DOWNLOAD_X64_URL="https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_x64.zip" +ARG DOWNLOAD_ARM64_URL="https://updates.networkoptix.com/default/42921/nxwitness-server_update-6.1.2.42921-linux_arm64.zip" +ARG DOWNLOAD_VERSION="6.1.2.42921" # Used for ${COMPANY_NAME} setting the server user and install directory ARG RUNTIME_NAME="networkoptix" diff --git a/HISTORY.md b/HISTORY.md index 5e58dd4..118d133 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.13: + - Fixed a regression bug that surfaced when Nx released an older version under the same tag, triggering the version-forward-release only logic. - 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: diff --git a/Make/Matrix.json b/Make/Matrix.json index 7194f01..4147835 100644 --- a/Make/Matrix.json +++ b/Make/Matrix.json @@ -375,13 +375,13 @@ "Branch": "main", "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/wisenetwave:6.0.5.41290", - "docker.io/ptr727/wisenetwave:stable" + "docker.io/ptr727/wisenetwave:6.1.1.42624", + "docker.io/ptr727/wisenetwave:latest" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_arm64.zip", - "DOWNLOAD_VERSION=6.0.5.41290", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_x64.zip" ] }, { @@ -391,7 +391,7 @@ "Base": "ubuntu", "Tags": [ "docker.io/ptr727/wisenetwave:6.1.2.42921", - "docker.io/ptr727/wisenetwave:latest" + "docker.io/ptr727/wisenetwave:stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", @@ -405,13 +405,13 @@ "Branch": "develop", "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/wisenetwave:develop-6.0.5.41290", - "docker.io/ptr727/wisenetwave:develop-stable" + "docker.io/ptr727/wisenetwave:develop", + "docker.io/ptr727/wisenetwave:develop-6.1.1.42624" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_arm64.zip", - "DOWNLOAD_VERSION=6.0.5.41290", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_x64.zip" ] }, { @@ -420,8 +420,8 @@ "Branch": "develop", "Base": "ubuntu", "Tags": [ - "docker.io/ptr727/wisenetwave:develop", - "docker.io/ptr727/wisenetwave:develop-6.1.2.42921" + "docker.io/ptr727/wisenetwave:develop-6.1.2.42921", + "docker.io/ptr727/wisenetwave:develop-stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", @@ -435,13 +435,13 @@ "Branch": "main", "Base": "lsio", "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:6.0.5.41290", - "docker.io/ptr727/wisenetwave-lsio:stable" + "docker.io/ptr727/wisenetwave-lsio:6.1.1.42624", + "docker.io/ptr727/wisenetwave-lsio:latest" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_arm64.zip", - "DOWNLOAD_VERSION=6.0.5.41290", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_x64.zip" ] }, { @@ -451,7 +451,7 @@ "Base": "lsio", "Tags": [ "docker.io/ptr727/wisenetwave-lsio:6.1.2.42921", - "docker.io/ptr727/wisenetwave-lsio:latest" + "docker.io/ptr727/wisenetwave-lsio:stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", @@ -465,13 +465,13 @@ "Branch": "develop", "Base": "lsio", "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:develop-6.0.5.41290", - "docker.io/ptr727/wisenetwave-lsio:develop-stable" + "docker.io/ptr727/wisenetwave-lsio:develop", + "docker.io/ptr727/wisenetwave-lsio:develop-6.1.1.42624" ], "Args": [ - "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_arm64.zip", - "DOWNLOAD_VERSION=6.0.5.41290", - "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_x64.zip" + "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_arm64.zip", + "DOWNLOAD_VERSION=6.1.1.42624", + "DOWNLOAD_X64_URL=https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_x64.zip" ] }, { @@ -480,8 +480,8 @@ "Branch": "develop", "Base": "lsio", "Tags": [ - "docker.io/ptr727/wisenetwave-lsio:develop", - "docker.io/ptr727/wisenetwave-lsio:develop-6.1.2.42921" + "docker.io/ptr727/wisenetwave-lsio:develop-6.1.2.42921", + "docker.io/ptr727/wisenetwave-lsio:develop-stable" ], "Args": [ "DOWNLOAD_ARM64_URL=https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", diff --git a/Make/Version.json b/Make/Version.json index 827aaa3..f59ae5e 100644 --- a/Make/Version.json +++ b/Make/Version.json @@ -75,11 +75,11 @@ "Product": "WisenetWAVE", "Versions": [ { - "Version": "6.0.5.41290", - "UriX64": "https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_x64.zip", - "UriArm64": "https://updates.networkoptix.com/hanwha/41290/wave-server_update-6.0.5.41290-linux_arm64.zip", + "Version": "6.1.1.42624", + "UriX64": "https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_x64.zip", + "UriArm64": "https://updates.networkoptix.com/hanwha/42624/wave-server_update-6.1.1.42624-linux_arm64.zip", "Labels": [ - "Stable" + "Latest" ] }, { @@ -87,7 +87,7 @@ "UriX64": "https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_x64.zip", "UriArm64": "https://updates.networkoptix.com/hanwha/42921/wave-server_update-6.1.2.42921-linux_arm64.zip", "Labels": [ - "Latest" + "Stable" ] } ] diff --git a/README.md b/README.md index 41acae6..8020a4c 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.12**: +**Version: 2.13**: **Summary**: -- 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. +- Fixed a regression bug that surfaced when Nx released an older version under the same tag, triggering the version-forward-release only logic. See [Release History](./HISTORY.md) for complete release notes and older versions. @@ -130,37 +130,36 @@ services: ## Table of Contents -- [Docker Projects for Network Optix VMS Products](#docker-projects-for-network-optix-vms-products) - - [Build and Distribution](#build-and-distribution) - - [Build Status](#build-status) - - [Release Notes](#release-notes) - - [Getting Started](#getting-started) - - [Table of Contents](#table-of-contents) - - [Products](#products) - - [Releases](#releases) - - [Overview](#overview) - - [Introduction](#introduction) - - [Base Images](#base-images) - - [LinuxServer](#linuxserver) - - [Configuration](#configuration) - - [LSIO Volumes](#lsio-volumes) - - [Non-LSIO Volumes](#non-lsio-volumes) - - [Ports](#ports) - - [Environment Variables](#environment-variables) - - [Network Mode](#network-mode) - - [Examples](#examples) - - [LSIO Docker Create](#lsio-docker-create) - - [LSIO Docker Compose](#lsio-docker-compose) - - [Non-LSIO Docker Compose](#non-lsio-docker-compose) - - [Unraid Template](#unraid-template) - - [Product Information](#product-information) - - [Release Information](#release-information) - - [Advanced Configuration](#advanced-configuration) - - [Build Process](#build-process) - - [Known Issues](#known-issues) - - [Troubleshooting](#troubleshooting) - - [Missing Storage](#missing-storage) - - [License](#license) +- [Build and Distribution](#build-and-distribution) + - [Build Status](#build-status) + - [Release Notes](#release-notes) +- [Getting Started](#getting-started) +- [Table of Contents](#table-of-contents) +- [Products](#products) +- [Releases](#releases) +- [Overview](#overview) + - [Introduction](#introduction) + - [Base Images](#base-images) + - [LinuxServer](#linuxserver) +- [Configuration](#configuration) + - [LSIO Volumes](#lsio-volumes) + - [Non-LSIO Volumes](#non-lsio-volumes) + - [Ports](#ports) + - [Environment Variables](#environment-variables) + - [Network Mode](#network-mode) +- [Examples](#examples) + - [LSIO Docker Create](#lsio-docker-create) + - [LSIO Docker Compose](#lsio-docker-compose) + - [Non-LSIO Docker Compose](#non-lsio-docker-compose) + - [Unraid Template](#unraid-template) +- [Product Information](#product-information) + - [Release Information](#release-information) + - [Advanced Configuration](#advanced-configuration) +- [Build Process](#build-process) +- [Known Issues](#known-issues) +- [Troubleshooting](#troubleshooting) + - [Missing Storage](#missing-storage) +- [License](#license) ## Products diff --git a/version.json b/version.json index 388dbef..97cc2cd 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.13", + "version": "2.14", "publicReleaseRefSpec": [ "^refs/heads/main$" ], From b028dec0589b921e1c79b6a645b9086ae1f31255 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 19 Jun 2026 10:33:29 -0700 Subject: [PATCH 09/12] Restore version floor to 2.13 (#437) The 2.13 release chore (merged in #436) bumped `version.json` to 2.14, but this release ships as **2.13** (see README/HISTORY). The next-cycle bump to 2.14 is a separate post-release step per AGENTS.md versioning, so restore the floor to 2.13. Only `version.json` changes; no image files, so no smoke build. Co-authored-by: Claude Opus 4.8 (1M context) --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 97cc2cd..388dbef 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.14", + "version": "2.13", "publicReleaseRefSpec": [ "^refs/heads/main$" ], From 4aac9e3e817c449933e6fd8ba207985dbfd133e9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 20 Jun 2026 08:05:09 -0700 Subject: [PATCH 10/12] Realign copilot-instructions.md with ProjectTemplate lean form (#439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the last open item of #418 (realign with `ptr727/ProjectTemplate`). Items 2–5 were already resolved by #422 and the rulesets are correct (`develop` squash-only, `main` merge-only, lowercase names); only `.github/copilot-instructions.md` still diverged. ## Change Carry the template's whole-file [`.github/copilot-instructions.md`](https://github.com/ptr727/ProjectTemplate/blob/main/.github/copilot-instructions.md) (verbatim drop-in): `# Copilot Instructions` → **Commit Messages and Pull Request Titles** → **GitHub Copilot Review Runbook** (unchanged from #422) → **When in Doubt**. Drops the NxWitness-specific preamble (Purpose, Solution Summary, CI Pipeline, standalone Versioning, etc.) — all already in [AGENTS.md](../blob/develop/AGENTS.md). ### Adaptations (per the carry contract) - Keep only the .NET `CODESTYLE.md` pointer (drop Python); drop devcontainer mentions (none here). - Fill `owner`/`repo` as `ptr727`/`NxWitness` in the runbook snippets; keep `` placeholders. - Retarget AGENTS.md cross-links to this repo's sections (`#release-model` → `#versioning`; keep `#pr-review-etiquette`). The template's "Files…Must Carry Verbatim" and "Staying in Sync" links point upstream, since this repo's AGENTS.md has no equivalent sections. ## Out of scope Versioning is unchanged — it already matches the template; making it functional is a separate template-level effort. ## Notes Doc-only, no code. An upstream issue will be filed against `ptr727/ProjectTemplate` re: the carried file linking to AGENTS.md sections a derived repo only has if it also carries the template's AGENTS.md structure. Closes #418. --- .github/copilot-instructions.md | 357 +++++++++++++++----------------- 1 file changed, 172 insertions(+), 185 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2c7434a..2167c31 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,185 +1,172 @@ -# GitHub Copilot Guidance - -## Purpose - -This file summarizes the solution and defines the hierarchy of guidance for AI-assisted contributions. - -## Guidance Hierarchy (Must Follow) - -1. [CODESTYLE.md](../CODESTYLE.md) is the master code style and formatting authority. -2. [AGENTS.md](../AGENTS.md) is secondary guidance describing the solution, workflows, and conventions. -3. Repository configuration files such as [`.editorconfig`](../.editorconfig) and [`.vscode/tasks.json`](../.vscode/tasks.json) define enforced formatting, line endings, and task expectations. - -If any instruction conflicts, follow CODESTYLE.md first, then AGENTS.md. - -## Solution Summary - -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, plus a .NET tooling project that generates Dockerfiles, matrices, and version inputs used by CI and packaging scripts. - -### Core Projects - -- `CreateMatrix` (.NET 10 console app): Generates Dockerfiles and build matrix data using version and release metadata. -- `CreateMatrixTests` (xUnit v3 + AwesomeAssertions): Validates release handling and version forwarding. - -### Key Inputs and Outputs - -- Inputs: version and matrix data in `version.json`, [Make/Version.json](../Make/Version.json), and [Make/Matrix.json](../Make/Matrix.json). -- Outputs: Dockerfiles in [Docker/](../Docker/) (base images and derived product images) and compose/test artifacts in [Make/](../Make/). -- Templates: Unraid container templates in [Unraid/](../Unraid/). - -### Build and Validation Workflow (High Level) - -- Primary entry points are the `CreateMatrix` CLI commands (version, matrix, make) run directly or via scripts in [Make/](../Make/). -- Formatting and style verification are enforced by CSharpier and dotnet format, with Husky.Net hooks. -- The `.Net Format` VS Code task in [`.vscode/tasks.json`](../.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 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 extensions recommended in the workspace file `NxWitness.code-workspace` (C#, Markdown, Docker, GitHub Actions, spelling) rather than a CI lint job; lint changed files before pushing, and run `actionlint` for deeper workflow checks. Editor settings, extension recommendations, and spell-check words belong in the workspace file (not `.vscode/`). See AGENTS.md. - -## What to Keep in Sync - -- Generated Dockerfiles and scripts must reflect CreateMatrix behavior. -- Base image Dockerfiles and derived image Dockerfiles should remain aligned since derived images build on the base images. -- Documentation in [README.md](../README.md) and release notes should align with current outputs and supported product variants. - -## Expectations for Changes - -- Follow the zero-warnings policy and formatting requirements in [CODESTYLE.md](../CODESTYLE.md). -- Use explicit types (no `var`), Allman braces, file-scoped namespaces, and other conventions as defined in the master style guide. -- Respect line endings and encoding rules from the repository configuration, including UTF-8 without BOM. - -## Versioning - -`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR, so develop's NBGV prerelease version (baked into its images as `LABEL_VERSION`) stays above main's last stable release. A `develop -> main` promotion that carries only maintenance (dependency bumps, CI/doc fixes, template re-syncs) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Versioning"](../AGENTS.md#versioning). - -## GitHub Copilot Review Runbook - -Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. - -### Triggering and Polling - -Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off). - -> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. - -```sh -# 1. PR node id + the Copilot reviewer's bot node id (read from any existing -# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). -PR_NODE=$(gh pr view --json id --jq '.id') -BOT_ID=$(gh api graphql -f query=' -{ - repository(owner: "ptr727", name: "NxWitness") { - pullRequest(number: ) { - reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } - } - } -}' --jq '[.data.repository.pullRequest.reviews.nodes[] - | select(.author.login == "copilot-pull-request-reviewer") - | .author.id] | first') - -# 2. Re-request a Copilot review on the current head. -gh api graphql -f query=' -mutation($pr: ID!, $bot: ID!) { - requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { - pullRequest { id } - } -}' -F pr="$PR_NODE" -F bot="$BOT_ID" -``` - -The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request. - -**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. - -Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead): - -- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. -- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. - -### Verify Review Covered Current Head - -Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both. - -```sh -PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') - -# 1. Formal review - exact SHA match. -gh pr view --json reviews --jq \ - '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ - | grep -q "$PR_HEAD" && echo "covered via formal review" - -# 2. Issue comment - show the most recent Copilot comment for manual -# confirmation. This is the REST API, so the login carries the `[bot]` suffix. -gh api repos/ptr727/NxWitness/issues//comments --jq \ - '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' -``` - -Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. - -### Bounded Retry Workflow - -If a review did not run on the current head, retry: - -1. Wait briefly and check head-SHA coverage (see above). -1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops. -1. Retry up to two more times (three total). -1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. - -### Reply and Thread Resolution Workflow - -List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: - -```sh -gh api graphql -f query=' -{ - repository(owner: "ptr727", name: "NxWitness") { - pullRequest(number: ) { - reviewThreads(first: 100) { - nodes { - id isResolved path - comments(first: 1) { nodes { author { login } body } } - } - pageInfo { hasNextPage endCursor } - } - } - } -}' | jq ' - .data.repository.pullRequest.reviewThreads | - (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), - (.nodes[] | select(.isResolved == false)) -' -``` - -Reply on a thread, then resolve it: - -```sh -gh api graphql -f query=' -mutation($threadId: ID!, $body: String!) { - addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { - comment { id } - } -}' -F threadId="PRRT_..." -F body="Fixed in : ." - -gh api graphql -f query=' -mutation($threadId: ID!) { - resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } -}' -F threadId="PRRT_..." -``` - -Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. - -Reply-body conventions: - -- Accepted bug/style fix: include fixing commit SHA and a one-line summary. -- Declined style comment: cite the rule (AGENTS.md or language CODESTYLE) and the existing-tree precedent. -- Declined architecture proposal: one-sentence rationale. - -After the final push, sweep-resolve stale older threads for removed code paths. +# Copilot Instructions + +Repository conventions for GitHub Copilot (and any other AI agent reading this file). + +The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first. It covers project layout, branch flow, PR review etiquette, the release pipeline, workflow conventions, coding conventions, and notes for changes. + +This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md. + +For language-specific style rules, see: + +- .NET - [`CODESTYLE.md`](../CODESTYLE.md) at the repo root. + +Do not duplicate language-specific rules here. + +## Commit Messages and Pull Request Titles + +Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages. + +`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR, so develop's prereleases sort above main's last stable. A `develop -> main` promotion that carries only maintenance (not a release) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Versioning"](../AGENTS.md#versioning). + +Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs. + +### Format + +- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".) +- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. + +### Rules + +- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.) +- Don't add `Co-Authored-By:` lines unless the user explicitly asks. +- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. +- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*). + +### Examples + +```text +Add structured logging extensions to library +Pin softprops/action-gh-release to commit SHA +Drop net8.0 multi-targeting from console project +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify LSIO volume configuration in README +``` + +## GitHub Copilot Review Runbook + +Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. + +### Triggering and Polling + +Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off). + +> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. + +```sh +# 1. PR node id + the Copilot reviewer's bot node id (read from any existing +# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). +PR_NODE=$(gh pr view --json id --jq '.id') +BOT_ID=$(gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "NxWitness") { + pullRequest(number: ) { + reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } + } + } +}' --jq '[.data.repository.pullRequest.reviews.nodes[] + | select(.author.login == "copilot-pull-request-reviewer") + | .author.id] | first') + +# 2. Re-request a Copilot review on the current head. +gh api graphql -f query=' +mutation($pr: ID!, $bot: ID!) { + requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { + pullRequest { id } + } +}' -F pr="$PR_NODE" -F bot="$BOT_ID" +``` + +The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request. + +**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. + +Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead): + +- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. +- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. + +### Verify Review Covered Current Head + +Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both. + +```sh +PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') + +# 1. Formal review - exact SHA match. +gh pr view --json reviews --jq \ + '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ + | grep -q "$PR_HEAD" && echo "covered via formal review" + +# 2. Issue comment - show the most recent Copilot comment for manual +# confirmation. This is the REST API, so the login carries the `[bot]` suffix. +gh api repos/ptr727/NxWitness/issues//comments --jq \ + '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' +``` + +Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. + +### Bounded Retry Workflow + +If a review did not run on the current head, retry: + +1. Wait briefly and check head-SHA coverage (see above). +1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops. +1. Retry up to two more times (three total). +1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. + +### Reply and Thread Resolution Workflow + +List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: + +```sh +gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "NxWitness") { + pullRequest(number: ) { + reviewThreads(first: 100) { + nodes { + id isResolved path + comments(first: 1) { nodes { author { login } body } } + } + pageInfo { hasNextPage endCursor } + } + } + } +}' | jq ' + .data.repository.pullRequest.reviewThreads | + (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), + (.nodes[] | select(.isResolved == false)) +' +``` + +Reply on a thread, then resolve it: + +```sh +gh api graphql -f query=' +mutation($threadId: ID!, $body: String!) { + addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { + comment { id } + } +}' -F threadId="PRRT_..." -F body="Fixed in : ." + +gh api graphql -f query=' +mutation($threadId: ID!) { + resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } +}' -F threadId="PRRT_..." +``` + +Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. + +Reply-body conventions: + +- Accepted bug/style fix: include fixing commit SHA and a one-line summary. +- Declined style comment: cite the rule (AGENTS.md or language CODESTYLE) and the existing-tree precedent. +- Declined architecture proposal: one-sentence rationale. + +After the final push, sweep-resolve stale older threads for removed code paths. + +## When in Doubt + +Read [AGENTS.md](../AGENTS.md) for the full picture (release flow, branching, workflow conventions, coding conventions, notes for changes). For language-specific rules, [`CODESTYLE.md`](../CODESTYLE.md) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. + +**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see [ProjectTemplate AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). From 8e42e6f6aebb16cfbea182e61ee0739994e5df28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:15:01 +0000 Subject: [PATCH 11/12] Bump the nuget-deps group with 1 update (#441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 18.6.0 to 18.7.0.
Release notes _Sourced from [Microsoft.NET.Test.Sdk's releases](https://github.com/microsoft/vstest/releases)._ ## 18.7.0 ## What's Changed * Add ARM64 msdia140.dll support to test platform packages by @​jamesmcroft in https://github.com/microsoft/vstest/pull/15689 * Update System.Memory from 4.5.5 to 4.6.3 by @​nohwnd in https://github.com/microsoft/vstest/pull/15706 ## New Contributors * @​jamesmcroft made their first contribution in https://github.com/microsoft/vstest/pull/15689 **Full Changelog**: https://github.com/microsoft/vstest/compare/v18.6.0...v18.7.0 Commits viewable in [compare view](https://github.com/microsoft/vstest/compare/v18.6.0...v18.7.0).
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Microsoft.NET.Test.Sdk&package-manager=nuget&previous-version=18.6.0&new-version=18.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- CreateMatrixTests/CreateMatrixTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CreateMatrixTests/CreateMatrixTests.csproj b/CreateMatrixTests/CreateMatrixTests.csproj index 46bfe26..506c2a6 100644 --- a/CreateMatrixTests/CreateMatrixTests.csproj +++ b/CreateMatrixTests/CreateMatrixTests.csproj @@ -9,7 +9,7 @@ - + all From 17e9a59674fd00617d0e09e515fc20dcf2d7c17c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 23 Jun 2026 18:37:49 -0700 Subject: [PATCH 12/12] Resync verbatim-carry files with upstream ProjectTemplate (#443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Periodic re-sync of the template-carried artifacts against [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) (tip `8a701a9`), plus the staged workspace dictionary/extension change. ## What changed - **`.github/copilot-instructions.md`** — adopt the template's current lean Copilot runbook: Merge Gate awareness, "a review with no inline comments is still a completed review", and round-1 auto-seed polling. This also fixes the file's LF line-ending drift (`.editorconfig` mandates CRLF for `.md`) and the regression from #439, which had realigned to an older/leaner form than the template now ships. Owner/repo placeholders filled; the per-language pointer drops Python (this repo is .NET-only). - **`AGENTS.md`** — carry the upgraded **PR Review Etiquette** contract verbatim (new **Merge Gate** subsection, "no-findings is a valid terminal outcome"). Added the **Pull Request Title and Commit Message Conventions** section that the lean runbook now links to. The `Release Model` anchor/`PUBLISH_ON_MERGE` reference were adapted to this repo's `CI Pipeline` section. Fixed a `.Net Format` → `.NET Format` task reference. - **`CODESTYLE.md`** — restructure to the template's **General + .NET** shape; drop the Python section; adapt the project list to `CreateMatrix`/`CreateMatrixTests` and `InternalsVisibleTo` accordingly. **Husky.Net is retained** as this repo's style gate (maintainer decision) — the template dropped it only because hooks don't fit non-.NET repos. - **`.editorconfig`, `.markdownlint-cli2.jsonc`** — pull current comments verbatim. - **`.vscode/tasks.json`** — rename `.Net` → `.NET`, add `dependsOrder: sequence` to `.NET Format`, keep the project-specific `Husky.Net Run` task; updated `.vscode/launch.json` and `.husky/task-runner.json` label references. - **`NxWitness.code-workspace`** — the staged change (`hddpool` dictionary word; swap `gruntfuggly.todo-tree` for `fanaticpythoner.better-todo-tree`). ## Notes - Versioning is unchanged; this is a maintenance re-sync. - Two upstream issues will be filed in `ptr727/ProjectTemplate`: (1) commit-gate confusion — Husky's absence is a non-.NET-fit concern, not a recommendation against gates; (2) verbatim-carry coherence gaps where carried files reference template-only sections/anchors (`#release-model`, the commit-conventions section, `#files-and-sections-derived-repos-must-carry-verbatim`). - Markdownlint clean on all three docs. --- .editorconfig | 8 +- .github/copilot-instructions.md | 326 +++++++++++------------- .github/workflows/publish-release.yml | 30 +++ .github/workflows/test-pull-request.yml | 29 +++ .husky/task-runner.json | 2 +- .markdownlint-cli2.jsonc | 3 +- .vscode/launch.json | 6 +- .vscode/tasks.json | 17 +- AGENTS.md | 52 +++- CODESTYLE.md | 153 ++++++----- NxWitness.code-workspace | 3 +- 11 files changed, 379 insertions(+), 250 deletions(-) diff --git a/.editorconfig b/.editorconfig index 3ad5391..e95c05b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,7 +36,7 @@ indent_size = 2 end_of_line = crlf indent_size = 2 -# Json files +# JSON and JSONC files [*.{json,jsonc}] end_of_line = crlf @@ -48,9 +48,15 @@ end_of_line = lf [*.{cmd,bat,ps1}] end_of_line = crlf +# --- .NET-only below: C# and ReSharper style. Everything above is the line-ending +# governance every derived repo carries; a non-.NET repo may drop from here down. --- + # C# files [*.cs] end_of_line = crlf +# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions": prefer a +# [SuppressMessage] attribute or the owning project's .editorconfig; relax a rule +# repo-wide here only when it applies to every project (never a brownfield batch). dotnet_diagnostic.IDE0055.severity = none dotnet_analyzer_diagnostic.severity = suggestion csharp_indent_block_contents = true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2167c31..55d823f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,172 +1,154 @@ -# Copilot Instructions - -Repository conventions for GitHub Copilot (and any other AI agent reading this file). - -The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first. It covers project layout, branch flow, PR review etiquette, the release pipeline, workflow conventions, coding conventions, and notes for changes. - -This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md. - -For language-specific style rules, see: - -- .NET - [`CODESTYLE.md`](../CODESTYLE.md) at the repo root. - -Do not duplicate language-specific rules here. - -## Commit Messages and Pull Request Titles - -Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages. - -`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR, so develop's prereleases sort above main's last stable. A `develop -> main` promotion that carries only maintenance (not a release) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Versioning"](../AGENTS.md#versioning). - -Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs. - -### Format - -- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".) -- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. - -### Rules - -- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.) -- Don't add `Co-Authored-By:` lines unless the user explicitly asks. -- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. -- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*). - -### Examples - -```text -Add structured logging extensions to library -Pin softprops/action-gh-release to commit SHA -Drop net8.0 multi-targeting from console project -Bump xunit.v3 from 3.2.2 to 3.3.0 -Clarify LSIO volume configuration in README -``` - -## GitHub Copilot Review Runbook - -Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. - -### Triggering and Polling - -Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off). - -> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. - -```sh -# 1. PR node id + the Copilot reviewer's bot node id (read from any existing -# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). -PR_NODE=$(gh pr view --json id --jq '.id') -BOT_ID=$(gh api graphql -f query=' -{ - repository(owner: "ptr727", name: "NxWitness") { - pullRequest(number: ) { - reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } - } - } -}' --jq '[.data.repository.pullRequest.reviews.nodes[] - | select(.author.login == "copilot-pull-request-reviewer") - | .author.id] | first') - -# 2. Re-request a Copilot review on the current head. -gh api graphql -f query=' -mutation($pr: ID!, $bot: ID!) { - requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { - pullRequest { id } - } -}' -F pr="$PR_NODE" -F bot="$BOT_ID" -``` - -The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request. - -**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. - -Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead): - -- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. -- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. - -### Verify Review Covered Current Head - -Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both. - -```sh -PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') - -# 1. Formal review - exact SHA match. -gh pr view --json reviews --jq \ - '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ - | grep -q "$PR_HEAD" && echo "covered via formal review" - -# 2. Issue comment - show the most recent Copilot comment for manual -# confirmation. This is the REST API, so the login carries the `[bot]` suffix. -gh api repos/ptr727/NxWitness/issues//comments --jq \ - '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' -``` - -Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. - -### Bounded Retry Workflow - -If a review did not run on the current head, retry: - -1. Wait briefly and check head-SHA coverage (see above). -1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops. -1. Retry up to two more times (three total). -1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. - -### Reply and Thread Resolution Workflow - -List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: - -```sh -gh api graphql -f query=' -{ - repository(owner: "ptr727", name: "NxWitness") { - pullRequest(number: ) { - reviewThreads(first: 100) { - nodes { - id isResolved path - comments(first: 1) { nodes { author { login } body } } - } - pageInfo { hasNextPage endCursor } - } - } - } -}' | jq ' - .data.repository.pullRequest.reviewThreads | - (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), - (.nodes[] | select(.isResolved == false)) -' -``` - -Reply on a thread, then resolve it: - -```sh -gh api graphql -f query=' -mutation($threadId: ID!, $body: String!) { - addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { - comment { id } - } -}' -F threadId="PRRT_..." -F body="Fixed in : ." - -gh api graphql -f query=' -mutation($threadId: ID!) { - resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } -}' -F threadId="PRRT_..." -``` - -Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. - -Reply-body conventions: - -- Accepted bug/style fix: include fixing commit SHA and a one-line summary. -- Declined style comment: cite the rule (AGENTS.md or language CODESTYLE) and the existing-tree precedent. -- Declined architecture proposal: one-sentence rationale. - -After the final push, sweep-resolve stale older threads for removed code paths. - -## When in Doubt - -Read [AGENTS.md](../AGENTS.md) for the full picture (release flow, branching, workflow conventions, coding conventions, notes for changes). For language-specific rules, [`CODESTYLE.md`](../CODESTYLE.md) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. - -**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see [ProjectTemplate AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). +# Copilot Instructions + +Repository conventions for GitHub Copilot (and any other AI agent reading this file). + +The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract this file's runbook implements. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook. + +For code-style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root - one guide with a General section plus per-language sections (.NET). + +Do not duplicate language-specific rules here. **Project-specific conventions and API/behavioral contracts also belong in [AGENTS.md](../AGENTS.md), not here** - this file is intentionally limited to the inline commit/PR-title summary and the GitHub Copilot Review Runbook. Non-Copilot agents (Claude Code, Codex, Cursor, ...) are not directed to this file and don't read it by default, so any rule a reviewer must honor has to live in `AGENTS.md` to be provider-independent. + +## Commit Messages and Pull Request Titles + +Summarized for VS Code's generators; the full rules, rationale, and examples are in [AGENTS.md "Pull Request Title and Commit Message Conventions"](../AGENTS.md#pull-request-title-and-commit-message-conventions). + +- Imperative subject, <= 72 characters, no trailing period; optional blank-line-separated body for the non-obvious *why*. +- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine. +- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection. + +## GitHub Copilot Review Runbook + +> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, fill the `` placeholder with the PR number. + +Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. + +### Triggering and Polling + +Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a UI hand-off. + +**A review with no inline comments is still a completed review - not a failure, and not a reason to ask the maintainer to re-trigger.** Copilot very often posts a single formal review (GraphQL `state: COMMENTED`) whose body ends with "...reviewed N of N changed files ... and generated no comments" and adds **zero** inline threads. That review carries the head `commit.oid` and fully satisfies the loop - it is the clean-pass success case. Never read "no inline comments" as "the review didn't run," and never re-request or escalate to the maintainer because comments are absent. + +**Round 1 is normally auto-seeded - poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA); by then a prior review exists, so its bot node id is readable. A missing bot node id on round 1 therefore means "the auto-review has not landed yet - wait and poll," **not** "ask the maintainer to kick it off." + +> **The reviewer login differs by API.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. + +```sh +# 1. PR node id + the Copilot reviewer's bot node id (read from any existing +# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). +PR_NODE=$(gh pr view --json id --jq '.id') +BOT_ID=$(gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "NxWitness") { + pullRequest(number: ) { + reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } + } + } +}' --jq '[.data.repository.pullRequest.reviews.nodes[] + | select(.author.login == "copilot-pull-request-reviewer") + | .author.id] | first') + +# 2. Re-request a Copilot review on the current head. +gh api graphql -f query=' +mutation($pr: ID!, $bot: ID!) { + requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { + pullRequest { id } + } +}' -F pr="$PR_NODE" -F bot="$BOT_ID" +``` + +The bot node id is read from an existing Copilot **formal** review (`pullRequest.reviews`), so step 1 needs at least one prior formal review on the PR - the auto-review-on-open normally supplies the first one (it may have **no inline comments**; that still counts, and its bot node id is still readable). Poll for it (give auto-review-on-open a few minutes) before deciding it is missing. If Copilot posted **only an issue comment** and no formal review, the head is covered but `reviews` yields no bot node id - read the id from the Copilot issue comment's author by querying the PR's issue comments in GraphQL (`pullRequest.comments` -> author `... on Bot { id }`), or request `Copilot` once through the GitHub PR UI to produce a formal review. Manual UI seeding is the fallback specifically when no formal review exists to read the id from; then use the mutation for every subsequent re-request. + +**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. + +Known non-working request paths (don't rely on them - use the `requestReviews` mutation above instead): + +- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. +- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. + +### Verify Review Covered Current Head + +Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA - use the most recent Copilot comment for manual confirmation). Check both. + +```sh +PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') + +# 1. Formal review - exact SHA match. +gh pr view --json reviews --jq \ + '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ + | grep -q "$PR_HEAD" && echo "covered via formal review" + +# 2. Issue comment - show the most recent Copilot comment for manual +# confirmation. This is the REST API, so the login carries the `[bot]` suffix. +gh api repos/ptr727/NxWitness/issues//comments --jq \ + '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' +``` + +Coverage is confirmed when (1) exits 0 - **a formal review with no inline comments still satisfies path (1)**, because coverage is about the head SHA, not the comment count. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. + +### Bounded Retry Workflow + +This path is only for a **genuinely missing** review - no Copilot review (formal *or* issue comment) covers the current head SHA after polling. A review that covered the head but produced no comments is a clean pass, not a missing review; do not enter this retry path for it. + +If a review did not run on the current head, retry: + +1. Wait briefly and check head-SHA coverage (see above). +1. Re-request the review via the `requestReviews` mutation (see "Triggering and Polling"); fall back to the GitHub PR UI only if the mutation no-ops. +1. Retry up to two more times (three total). +1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. + +### Reply and Thread Resolution Workflow + +List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: + +```sh +gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "NxWitness") { + pullRequest(number: ) { + reviewThreads(first: 100) { + nodes { + id isResolved path + comments(first: 1) { nodes { author { login } body } } + } + pageInfo { hasNextPage endCursor } + } + } + } +}' | jq ' + .data.repository.pullRequest.reviewThreads | + (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), + (.nodes[] | select(.isResolved == false)) +' +``` + +Reply on a thread, then resolve it: + +```sh +gh api graphql -f query=' +mutation($threadId: ID!, $body: String!) { + addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { + comment { id } + } +}' -F threadId="PRRT_..." -F body="Fixed in : ." + +gh api graphql -f query=' +mutation($threadId: ID!) { + resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } +}' -F threadId="PRRT_..." +``` + +Issue-level Copilot comments (those in `issues//comments`) have no resolution action - GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. + +Reply-body conventions: + +- Accepted bug/style fix: include fixing commit SHA and a one-line summary. +- Declined style comment: cite the rule (AGENTS.md or the CODESTYLE.md language section) and the existing-tree precedent. +- Declined architecture proposal: one-sentence rationale. + +After the final push, sweep-resolve stale older threads for removed code paths. + +## When in Doubt + +Read [AGENTS.md](../AGENTS.md) for this repo's conventions. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the relevant language section) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. + +**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 53f39f2..fa6121a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -157,3 +157,33 @@ jobs: secrets: inherit permissions: contents: write + + # Workflow artifacts are an intra-run handoff (durable copies live on the + # GitHub release and Docker Hub), so leaving them accumulates against the small + # account-wide storage quota; delete them once every consumer has read them. + # This publisher always publishes when it runs (schedule/dispatch only), so no + # publish gate is needed beyond always(). + cleanup-artifacts: + name: Delete workflow artifacts job + needs: [build-base, build-main, build-develop, github-release, docker-readme, date-badge] + if: always() + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete workflow artifacts step + # continue-on-error: best-effort housekeeping must never red the run, even on an unexpected failure. + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \ + --jq '.artifacts[].id'); then + echo "::warning::Could not list run artifacts; skipping cleanup (storage may not be freed)." + ids="" + fi + for artifact_id in $ids; do + gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \ + || echo "::warning::Failed to delete artifact $artifact_id; continuing." + done diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 260f588..359f03b 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -102,3 +102,32 @@ jobs: 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 }}" + + # The smoke build runs docker/build-push-action, which can emit a build-record + # artifact, so this terminal cleanup deletes the run's artifacts to keep them + # off the small account-wide storage quota. Independent of + # check-workflow-status so housekeeping never gates the required merge check. + cleanup-artifacts: + name: Delete workflow artifacts job + needs: [smoke-build] + if: always() + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete workflow artifacts step + # continue-on-error: best-effort housekeeping must never red the run, even on an unexpected failure. + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \ + --jq '.artifacts[].id'); then + echo "::warning::Could not list run artifacts; skipping cleanup (storage may not be freed)." + ids="" + fi + for artifact_id in $ids; do + gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \ + || echo "::warning::Failed to delete artifact $artifact_id; continuing." + done diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 009e6b3..dbcd960 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -15,7 +15,7 @@ ] }, { - "name": ".Net Format", + "name": ".NET Format", "command": "dotnet", "args": [ "format", diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index c6a5714..4afb100 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -7,7 +7,8 @@ "MD033": false, // Require fenced code blocks over the legacy 4-space-indented style. "MD046": { "style": "fenced" }, - // Wide tables are intentional where wrapping cells breaks GitHub rendering. + // MD060 (table column style) is not enforced - allow both compact + // (`|a|b|`) and padded (`| a | b |`) table pipe spacing. "MD060": false }, "gitignore": true diff --git a/.vscode/launch.json b/.vscode/launch.json index f6706d7..660c933 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Create Version", "type": "coreclr", "request": "launch", - "preLaunchTask": ".Net Build", + "preLaunchTask": ".NET Build", "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", "args": ["version", "--versionpath=./Make/Version.json"], "cwd": "${workspaceFolder}", @@ -16,7 +16,7 @@ "name": "Create Matrix", "type": "coreclr", "request": "launch", - "preLaunchTask": ".Net Build", + "preLaunchTask": ".NET Build", "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", "args": ["matrix", "--versionpath=./Make/Version.json", "--matrixpath=./Make/Matrix.json", "--updateversion"], "cwd": "${workspaceFolder}", @@ -27,7 +27,7 @@ "name": "Create Docker and Compose Files", "type": "coreclr", "request": "launch", - "preLaunchTask": ".Net Build", + "preLaunchTask": ".NET Build", "program": "${workspaceFolder}/CreateMatrix/bin/Debug/net10.0/CreateMatrix.dll", "args": ["make", "--versionpath=./Make/Version.json", "--makedirectory=./Make", "--dockerdirectory=./Docker", "--versionlabel=Beta"], "cwd": "${workspaceFolder}", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 91ab548..6c819ef 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,8 +1,11 @@ { "version": "2.0.0", "tasks": [ + // .NET language group. A non-.NET repo drops this group and adds its own + // language's tasks. The first three tasks are the .NET clean-compile set + // (CODESTYLE.md) carried verbatim; the rest are convenience/project-specific. { - "label": ".Net Build", + "label": ".NET Build", "type": "process", "command": "dotnet", "args": [ @@ -20,7 +23,7 @@ } }, { - "label": ".Net Format", + "label": ".NET Format", "type": "process", "command": "dotnet", "args": [ @@ -37,9 +40,10 @@ "showReuseMessage": false, "clear": false }, + "dependsOrder": "sequence", "dependsOn": [ "CSharpier Format", - ".Net Build" + ".NET Build" ] }, { @@ -60,8 +64,9 @@ "clear": false } }, + // Convenience / project-specific tasks (adapt or drop per repo). { - "label": ".Net Tool Update", + "label": ".NET Tool Update", "type": "process", "command": "dotnet", "args": [ @@ -94,12 +99,12 @@ } }, { - "label": ".Net Outdated Upgrade", + "label": ".NET Outdated Upgrade", "type": "process", "command": "dotnet", "args": [ "outdated", - "--upgrade:Prompt" + "--upgrade:prompt" ], "problemMatcher": [ "$msCompile" diff --git a/AGENTS.md b/AGENTS.md index 149c55a..895739f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ For comprehensive coding and formatting standards, follow: - 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. +- The `.NET Format` VS Code task in `.vscode/tasks.json` must be clean and warning-free at all times. ### Workspace and linting @@ -63,21 +63,67 @@ For comprehensive coding and formatting standards, follow: The `version` (major.minor) in [version.json](./version.json) is the NBGV version floor; NBGV appends the git height. **`develop` leads `main` by a minor:** after a `develop -> main` release lands and main's publish completes, bump the minor in `version.json` on `develop` in an isolated `bump-version-X.Y` PR (X.Y = the new minor), so develop's NBGV prerelease version stays numerically above main's last stable. A **maintenance** `develop -> main` promotion (dependency bumps, CI/doc fixes, template re-syncs) holds main's version - `git checkout main -- version.json` on the promotion branch - so `main` advances only its NBGV height, not its minor. (NBGV's version is the GitHub release tag on `main` and the `LABEL_VERSION` build arg baked into the images; the Docker image *tags* carry the Nx product version from `Make/Matrix.json` - see [CI Pipeline](#ci-pipeline-github-actions).) +## Git and Commit Rules + +- **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound - it covers the commits needed for that specific task, not a blanket commit license for the rest of the session. +- **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration - `git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent (loaded `ssh-agent` for SSH, or `gpg-agent` for GPG). If signing is not configured in the environment, **do not commit** - surface the missing config to the developer and stop at `git add`. Verify before any agent-authored commit (`git config --get commit.gpgsign && ssh-add -L` or the GPG equivalent). **Signing must be live before the *first* commit, not retrofitted.** Turning on `Require signed commits` against a branch that already has unsigned commits forces a rewrite of that entire history to re-sign it - changing every commit SHA and making whoever does the rewrite the committer and signer of every commit (a rebase preserves the `author` field but not the original signatures; you cannot sign another contributor's commits for them). During new-repo setup, never create commits until signing is verified. +- **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. +- **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. + +## Pull Request Title and Commit Message Conventions + +### Format + +- Imperative subject summarizing the change, <=72 characters, no trailing period. ("Add NxMeta LSIO image variant", not "Added X" or "Adds X".) +- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. + +### Rules + +- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.) +- Don't add `Co-Authored-By:` lines unless the developer explicitly asks. +- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. Nerdbank.GitVersioning computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. +- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*). + +### Examples + +```text +Add Wisenet WAVE product variant +Pin softprops/action-gh-release to commit SHA +Drop legacy Ubuntu base image tag +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify LSIO volume configuration in README +``` + ## PR Review Etiquette +> **Mandatory in every derived repo.** This entire "PR Review Etiquette" section is the provider-agnostic review-loop *contract* and must be carried **verbatim** into every repo derived from this template, alongside the [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) "GitHub Copilot Review Runbook" that implements it. Without both in-repo, an agent working in the derived repo has no pointer to the reliable Copilot mechanics and falls back to ad-hoc (and known-broken) behavior. + The repo runs a review loop on every PR: local agent iteration plus remote automated review (GitHub Copilot is the configured reviewer). Treat this as a contract regardless of which local agent authored the changes. +### Merge Gate (read this first) + +**Do not merge - and do not enable auto-merge - unless ALL of these hold:** + +1. Required status checks are green (`mergeStateStatus: CLEAN`), **and** +2. A Copilot review is confirmed on the **current head SHA** (not an earlier push), **and** +3. **Every** Copilot finding on that head SHA is closed out - all review threads resolved, **and** any issue-level Copilot comments (which have no resolve action) triaged and replied to - so zero outstanding findings remain, **and** +4. The maintainer has given **explicit** permission to merge. + +`mergeStateStatus: CLEAN` reflects **only** required statuses - it never reflects open bot review comments, so `CLEAN` alone is **never** sufficient to merge. A green/`CLEAN` PR with an unresolved Copilot finding fails this gate; treat it as "not mergeable" no matter what the merge-state field says. The agent never merges on its own (consistent with "default to staging"; merging is maintainer-authorized). + +**Merging is not releasing.** A merge to a release branch does **not** by itself publish; publishing is a separate step in the repo's release pipeline (a scheduled run or a manual dispatch), not an automatic consequence of merging. Never describe a merge as cutting a release, and never trigger a publish without explicit maintainer instruction. + ### Expected Review Loop 1. Push changes to the PR branch. 2. Re-request a review for the **current head SHA**. Auto-trigger is unreliable, so request it explicitly via the `requestReviews` GraphQL mutation (now reliable end-to-end - see the runbook); the UI is only a fallback. -3. Wait for review activity on that head. +3. Wait for review activity on that head. A completed review that raises **no findings** is a valid terminal outcome for that head - proceed; do not re-trigger it or treat the absence of comments as a missing review. 4. Triage findings. 5. Apply fixes or write a rationale for declines. 6. Reply to each thread and resolve what was addressed. 7. Re-run the loop after every fix push until no actionable findings remain. -`mergeStateStatus: CLEAN` only checks required statuses; it does not block on bot review comments. Drive the loop to green - review confirmed on the latest head SHA and every actionable finding closed - and then **wait for the maintainer's explicit permission to merge**. The agent does not merge on its own (consistent with "default to staging"; merging is maintainer-authorized). +Drive the loop to green - review confirmed on the latest head SHA and every actionable finding closed - then stop and apply the **Merge Gate** above: all four preconditions must hold, and `mergeStateStatus: CLEAN` alone never satisfies it. For provider-specific mechanics (how to request review, query review state, post replies, resolve threads), see the **GitHub Copilot Review Runbook** in [.github/copilot-instructions.md](./.github/copilot-instructions.md). This file owns the contract; that file owns the mechanics. diff --git a/CODESTYLE.md b/CODESTYLE.md index 8b42aa6..abf38a8 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -1,74 +1,101 @@ # Code Style and Formatting Rules -## Build Requirements +This is the single code-style guide for the repo. The **General** section applies to every language and is always carried. This repo ships only a .NET side, so it carries the **General** and **.NET** sections; the template's Python section is dropped - the same per-language model as [`.editorconfig`](./.editorconfig), whose `[*.cs]` block a non-.NET repo drops. -### Zero Warnings Policy +Cross-cutting *process* rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and are not repeated here. + +## General + +These rules apply to every language in the repo. + +### Tooling Names and Casing + +Use each tool's official casing in task labels, docs, and prose - `.NET` (not `.Net`), `CSharpier`. Don't invent personal variants. + +### Clean-Compile Verification + +Each language defines a **clean-compile** verification - the combination of build, formatter, linter, and code-analysis tools that must report clean before a commit. It is exposed as one or more **named** VS Code tasks (or, where a language ships no tasks, documented commands), and those definitions are **carried verbatim** across derived repos. The concrete names live in each language section below. + +- **Run it after every code change.** The relevant language's clean-compile must pass before you commit; CI runs the same checks as a backstop. +- **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly; either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). +- **A local commit/pre-commit gate is the derived repo's choice - the template ships no hook runner only because no single runner fits every language it targets** (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), **not** as a recommendation against commit gates. CI is the authoritative backstop regardless; a local gate is an additive convenience a repo may wire and keep - Husky.Net (and `dotnet husky run` as a style step) for .NET, `pre-commit` for Python. Keeping a working gate is not drift, and "no hooks ship by default" must not be read as "remove your gate to stay aligned". + +### Analyzer Diagnostics and Suppressions + +- **A new port is not a license to silence diagnostics.** Brownfield / just-ported status never justifies relaxing analyzer or linter severities or muting newly surfaced warnings - fix them. (The only brownfield allowance in this template is the one-time git-signing / line-ending migration described in [AGENTS.md](./AGENTS.md) and [README.md](./README.md), which has nothing to do with code analysis.) +- **Suppress only genuine false-positives or deliberate, documented exceptions**, always at the **narrowest scope that fits**, in this order of preference: + 1. An **in-code annotation on the specific symbol**, with a justification - the language's attribute/comment form, never a blanket pragma spanning a region. + 2. The **owning project's local config** when the exception is project-wide for one project (e.g. a test project's own `.editorconfig`). + 3. The **root / shared config** only when the suppression is genuinely applicable to **every** project in the repo. +- **Never blanket-relax a batch of rules project-wide** to get a port to build. The per-language mechanics (which attribute, which config key) are in each language section. + +### Markdown and Spelling + +These apply repo-wide, in every directory: + +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. This file is carried verbatim by every derived repo (see the template's [Files and Sections Derived Repos Must Carry Verbatim](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#files-and-sections-derived-repos-must-carry-verbatim) list). Fix violations at the source rather than disabling rules. +2. **Spelling**: All spelling must be clean via the CSpell VS Code integration; words must be correctly spelled in **US English** (the repo-wide convention - see [AGENTS.md](./AGENTS.md)). Project-specific terms go in the workspace CSpell config. + +## .NET + +This is the style guide for the **.NET projects** in this repo: [`CreateMatrix/`](./CreateMatrix/) (the console app) and [`CreateMatrixTests/`](./CreateMatrixTests/) (the xUnit test project). + +### Build Requirements + +#### Zero Warnings Policy **CRITICAL**: All builds must complete without warnings. The project enforces this through: -1. **VS Code tasks** - - `CSharpier Format` → `.Net Build` → `.Net Format` - - `.Net Format` must pass with `--verify-no-changes` before commit - - Command: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` +1. **The `.NET Format` clean-compile task** (see [Clean-Compile Verification](#clean-compile-verification)) + - The .NET clean-compile is the **`.NET Format`** VS Code task, which chains `CSharpier Format` -> `.NET Build` -> `dotnet format style --verify-no-changes`. These three task definitions are carried verbatim in [`.vscode/tasks.json`](./.vscode/tasks.json). + - After any code change it must pass before commit. Run the `.NET Format` task. To run it natively instead, reproduce that task chain from [`.vscode/tasks.json`](./.vscode/tasks.json) exactly - `CSharpier Format`, then `.NET Build`, then the `dotnet format style --verify-no-changes --severity=info ...` verify - without dropping or loosening any argument (tasks.json is the canonical command spec). Bare `dotnet format` alone, skipping CSharpier or the build, is not sufficient. 2. **Analyzer configuration** - `latest-all` - `true` - - Analyzer severity is `suggestion`, but all warnings must be addressed + - Analyzer severity is `suggestion`, but all warnings must be addressed - see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions); do not relax rules to dodge them. -3. **Husky.Net pre-commit hooks** - - Automated checks run before commits +3. **Husky.Net pre-commit hooks and CI backstop** + - `dotnet husky run` runs the [`.husky/task-runner.json`](./.husky/task-runner.json) tasks (`CSharpier Format`, then `.NET Format`) before each commit + - CI runs the same `dotnet husky run` on every PR as a backstop -### Build Tasks +#### Build Tasks -Available VS Code tasks (use via `run_task` tool): +Available VS Code tasks (run them from VS Code's task runner - **Terminal -> Run Task** - or an agent's task-running tool). The first three are the clean-compile set, carried verbatim; the rest are convenience/project-specific tasks a derived repo adapts or drops: -- `.Net Build`: Build with diagnostic verbosity -- `.Net Format`: Verify formatting and style (must pass) -- `CSharpier Format`: Auto-format code with CSharpier -- `.Net Tool Update`: Update dotnet tools -- `Husky.Net Run`: Run pre-commit hooks manually +- `.NET Build`: Build with diagnostic verbosity *(clean-compile)* +- `CSharpier Format`: Auto-format code with CSharpier *(clean-compile)* +- `.NET Format`: Run CSharpier and build, then verify formatting and style with `--verify-no-changes` *(clean-compile; the task to run after edits)* +- `.NET Tool Update`: Update dotnet tools *(convenience)* +- `.NET Outdated Upgrade`: Upgrade outdated NuGet dependencies, interactive prompt *(convenience)* +- `Husky.Net Run`: Run the pre-commit hooks manually *(project-specific)* -## Tooling and Editor +### Tooling and Editor -### Code Formatting and Tooling +#### Code Formatting and Tooling 1. **CSharpier**: Primary code formatter - - Run before committing: `dotnet csharpier format --log-level=debug .` - + - Invoked by the `CSharpier Format` task / `dotnet csharpier format --log-level=debug .` 2. **dotnet format**: Style verification - Verify no changes: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` - 3. **Husky.Net**: Git hooks for automated checks - Installed as a local dotnet tool (via `dotnet tool restore`) - Install Git hooks locally with `dotnet husky install` - - Pre-commit hooks run formatting and style checks - + - Pre-commit hooks ([`.husky/task-runner.json`](./.husky/task-runner.json)) run CSharpier and `dotnet format style` 4. **Other tools** - `dotnet-outdated-tool`: Dependency update checks - Nerdbank.GitVersioning: Version management -### Editor Baseline +#### Editor Baseline 1. **Required VS Code extensions**: CSharpier, markdownlint, CSpell 2. **VS Code settings**: Use the workspace settings without overrides -### Markdown Files - -1. **Linting**: All `.md` files must be linted with the VS Code `markdownlint` extension (local only; no CI) -2. **Zero warnings**: Markdown linting must be error and warning free - -### Spelling - -1. **CSpell**: All spelling checks must be error free using the CSpell VS Code integration -2. **Accepted spellings**: Words must be correctly spelled in US or UK English -3. **Allowed exceptions**: Project-specific terms must be added to the workspace CSpell config - -## Coding Standards and Conventions +### Coding Standards and Conventions Note: Code snippets are illustrative examples only. Replace namespaces/types to match your project. -### C# Language Features +#### C# Language Features 1. **File-scoped namespaces** @@ -104,7 +131,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to var name = "test"; ``` -### Naming Conventions +#### Naming Conventions 1. **Private fields**: underscore prefix with camelCase @@ -125,7 +152,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to private const int MaxRetries = 3; ``` -### Code Structure +#### Code Structure 1. **Global usings**: Use `GlobalUsings.cs` for common namespaces @@ -169,11 +196,9 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to - Linux scripts (`.sh`): LF 6. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization. -7. **Member ordering (StyleCop SA1201)**: const → static readonly → static fields → instance readonly fields → instance fields → constructors → public (events → properties → indexers → methods → operators) → non-public in same order → nested types - -8. **File encoding**: UTF-8, no BOM's. +7. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types -### Comments and Documentation +#### Comments and Documentation 1. **XML documentation** - `true` @@ -204,20 +229,25 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to public async Task GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {} ``` -2. **Code analysis suppressions** - - Do not use `#pragma` sections to disable analyzers - - For one-off cases, use suppression attributes with justifications - - For project-wide suppressions, add rules to `.editorconfig` +#### Analyzer Suppressions (.NET) - ```csharp - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Design", - "CA1034:Nested types should not be visible", - Justification = "https://github.com/dotnet/sdk/issues/51681" - )] - ``` +Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). .NET mechanics, narrowest first: + +- **Never use `#pragma warning disable`** to silence an analyzer. +- **Symbol-scoped**: a `[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]` attribute with a `Justification`, on the specific member or type: + + ```csharp + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "https://github.com/dotnet/sdk/issues/51681" + )] + ``` + +- **Project-scoped** (e.g. a test project): a `dotnet_diagnostic..severity` entry in *that project's own* `.editorconfig`, with a comment explaining why. +- **Repo-wide**: a `dotnet_diagnostic..severity` entry in the root `.editorconfig`, only when the rule is genuinely not applicable to any project. Relaxing a batch of `CA*` rules (or `dotnet_analyzer_diagnostic.severity`) to push a brownfield port through the build is exactly what this forbids. -### Error Handling and Logging +#### Error Handling and Logging 1. **Serilog logging**: Use structured logging @@ -249,7 +279,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 5. **Exceptions**: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception -### Code Patterns +#### Code Patterns 1. **Guard clauses**: Prefer early returns for validation and error handling 2. **Async all the way**: Avoid blocking calls (`.Result`, `.Wait()`); use `async`/`await` @@ -266,7 +296,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 12. **Read-only data**: Use immutable or frozen collections for read-only data sets 13. **Lazy initialization**: Use `Lazy` for static, thread-safe instantiation (e.g., logger factory, HTTP factory) -### Testing Conventions +#### Testing Conventions 1. **Framework**: xUnit with AwesomeAssertions @@ -289,7 +319,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 3. **Naming**: Descriptive names with underscores 4. **Theory tests**: Use `[Theory]` with `[InlineData]` -## Project Configuration +### Project Configuration 1. **Target framework**: .NET 10.0 (`net10.0`) @@ -302,15 +332,14 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to - Include SourceLink: `true` - Embed untracked sources: `true` -4. **Internal visibility**: Use `InternalsVisibleTo` for test and benchmark access +4. **Internal visibility**: Use `InternalsVisibleTo` for test access (adapt the project name to your repo's test project) ```xml - - + ``` -## Best Practices +### Best Practices 1. **Code reviews**: All changes go through pull requests diff --git a/NxWitness.code-workspace b/NxWitness.code-workspace index abb70cd..9cc9350 100644 --- a/NxWitness.code-workspace +++ b/NxWitness.code-workspace @@ -35,6 +35,7 @@ "getmatrix", "gruntfuggly", "Hanwha", + "hddpool", "IPVMS", "kinnairdclan", "lsio", @@ -130,11 +131,11 @@ "davidanson.vscode-markdownlint", "editorconfig.editorconfig", "github.vscode-github-actions", - "gruntfuggly.todo-tree", "ms-azuretools.vscode-docker", "ms-dotnettools.csdevkit", "streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one", + "fanaticpythoner.better-todo-tree", ] } }