From 99bdf7f23f0734e01ea36ac5d0d3b09abe2c110a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:52:27 +0000 Subject: [PATCH 1/2] Initial plan From 47a27f36444fe48f121665ba88afdce4723b2cbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:00:13 +0000 Subject: [PATCH 2/2] Harden extension registry CI: validate dev registry and guard workflow path drift Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- .github/scripts/src/ext-registry-check.js | 14 +++++++----- .../scripts/test/ext-registry-check.test.js | 22 +++++++++++++++++++ .github/workflows/ext-registry-ci.yml | 11 +++++----- .github/workflows/scripts-ci.yml | 3 +++ cli/azd/pkg/extensions/registry_files_test.go | 9 +++++++- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/.github/scripts/src/ext-registry-check.js b/.github/scripts/src/ext-registry-check.js index 103c1232aec..d4ece6c0783 100644 --- a/.github/scripts/src/ext-registry-check.js +++ b/.github/scripts/src/ext-registry-check.js @@ -3,6 +3,14 @@ // simple changes (simple version bump, no changes to important fields) can go by with just a simple approval from any developer. const { isDeepStrictEqual } = require('node:util'); +// The registries this check governs. The ext-registry-check workflow keeps an inline +// copy of this list (it runs its detection step before the checkout, so it can't +// require this file); a test asserts the two stay in sync. +const REGISTRY_JSON_PATHS = new Set([ + 'cli/azd/extensions/registry.json', + 'cli/azd/extensions/registry.dev.json', +]); + // GitHub Actions entry point. module.exports = run; @@ -14,13 +22,9 @@ module.exports.forTests = { isAllowedRegistryJsonUpdate, isCreatedByCoreTeam, diffRegistry, + REGISTRY_JSON_PATHS, } -const REGISTRY_JSON_PATHS = new Set([ - 'cli/azd/extensions/registry.json', - 'cli/azd/extensions/registry.dev.json', -]); - // We only allow URLs that point to our GitHub releases page. const ALLOWED_ARTIFACT_URL_ORIGIN = 'https://github.com'; const ALLOWED_ARTIFACT_URL_PATH_PREFIX = '/Azure/azure-dev/releases/download/'; diff --git a/.github/scripts/test/ext-registry-check.test.js b/.github/scripts/test/ext-registry-check.test.js index e0c8a85407f..702746db6ae 100644 --- a/.github/scripts/test/ext-registry-check.test.js +++ b/.github/scripts/test/ext-registry-check.test.js @@ -1,4 +1,6 @@ import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { describe, it, expect, vi } from 'vitest'; import run from '../src/ext-registry-check.js'; @@ -35,6 +37,7 @@ const { diffRegistry, getCoreReviewers, isAllowedRegistryJsonUpdate, + REGISTRY_JSON_PATHS, } = run.forTests; const PROD_REGISTRY_PATH = 'cli/azd/extensions/registry.json'; @@ -1024,6 +1027,25 @@ describe('getCoreReviewers', () => { }); }); +describe('REGISTRY_JSON_PATHS', () => { + // The ext-registry-check workflow detects registry changes before it checks out the + // repo, so it can't require this script and keeps its own copy of the path list. + // A path added here but missed there fails open: the gated steps skip and the + // required check reports green without the policy ever running. + it('matches the inline registryPaths list in ext-registry-check.yml', () => { + const workflowPath = join(__dirname, '..', '..', 'workflows', 'ext-registry-check.yml'); + const workflow = readFileSync(workflowPath, 'utf8'); + + const setLiteral = /const registryPaths = new Set\(\[([^\]]*)\]\)/.exec(workflow); + // Fail loudly rather than silently stop guarding if the literal is renamed or restructured. + expect(setLiteral, 'could not find the `registryPaths` Set literal in ext-registry-check.yml').not.toBeNull(); + + const workflowPaths = [...(setLiteral?.[1] ?? '').matchAll(/'([^']+)'/g)].map((match) => String(match[1])); + expect(workflowPaths.length).toBeGreaterThan(0); + expect(workflowPaths.sort()).toEqual([...REGISTRY_JSON_PATHS].sort()); + }); +}); + /** * @param {{ * base?: RegistryJson, diff --git a/.github/workflows/ext-registry-ci.yml b/.github/workflows/ext-registry-ci.yml index 1b1d390dbfa..ade10a79745 100644 --- a/.github/workflows/ext-registry-ci.yml +++ b/.github/workflows/ext-registry-ci.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - "cli/azd/extensions/registry.json" + - "cli/azd/extensions/registry.dev.json" - ".github/workflows/ext-registry-ci.yml" branches: [main] @@ -34,16 +35,16 @@ jobs: - name: Build azd run: go build . - # Validates registry.json and fails on any hard validation error. The most - # important case is a declared dependency constraint that no published version + # Validates the extension registries and fails on any hard validation error. The + # most important case is a declared dependency constraint that no published version # satisfies, which is the inconsistency a coordinated multi-extension bump can # introduce, but schema, id, and artifact errors also fail here. Warnings (e.g. # an absent dependency id) do not fail this step, so routine extension releases - # are not blocked. Runs on the same trigger as registry.json. + # are not blocked. Runs on the same trigger as the registry files. - name: Validate registry dependency consistency run: | - if ! go test ./pkg/extensions -run '^TestRegistryFileIsValid$' -v; then - echo "::error::registry.json failed validation. See the test output above for the specific error(s)." + if ! go test ./pkg/extensions -run '^TestRegistryFileIsValid$|^TestDevRegistryFileIsValid$' -v; then + echo "::error::An extension registry failed validation. See the test output above for the specific error(s)." echo "Suggestion: a common cause is a dependency pinned to a version that is not yet published in the registry; ensure that version is published before updating the dependent extension." exit 1 fi diff --git a/.github/workflows/scripts-ci.yml b/.github/workflows/scripts-ci.yml index c2cdadb6cd6..5cdf6f0436d 100644 --- a/.github/workflows/scripts-ci.yml +++ b/.github/workflows/scripts-ci.yml @@ -5,6 +5,9 @@ on: paths: - ".github/scripts/**" - ".github/workflows/scripts-ci.yml" + # The script tests assert that the ext-registry-check workflow's inline registry + # path list matches the script's, so changes to that workflow run them too. + - ".github/workflows/ext-registry-check.yml" # If two events are triggered within a short time in the same PR, cancel the run of the oldest event concurrency: diff --git a/cli/azd/pkg/extensions/registry_files_test.go b/cli/azd/pkg/extensions/registry_files_test.go index 7db3e371292..29469fd50c7 100644 --- a/cli/azd/pkg/extensions/registry_files_test.go +++ b/cli/azd/pkg/extensions/registry_files_test.go @@ -35,6 +35,9 @@ func collectValidationErrors(result *RegistryValidationResult) []string { return errs } +// TestDevRegistryFileIsValid gates the development registry on the same +// ext-registry-ci trigger as TestRegistryFileIsValid, and applies the same rule: +// only validation errors fail, never warnings. func TestDevRegistryFileIsValid(t *testing.T) { registryPath := filepath.Join("..", "..", "extensions", "registry.dev.json") data, err := os.ReadFile(registryPath) @@ -45,7 +48,11 @@ func TestDevRegistryFileIsValid(t *testing.T) { require.Equal(t, CurrentRegistrySchemaVersion, registry.SchemaVersion) result := ValidateRegistry(®istry, false) - require.True(t, result.Valid, "registry.dev.json failed validation: %+v", result) + if !result.Valid { + errs := collectValidationErrors(result) + t.Fatalf("registry.dev.json failed validation with %d error(s):\n - %s", + len(errs), strings.Join(errs, "\n - ")) + } } // TestRegistryFileIsValid gates the production registry on every pull request that