diff --git a/.github/workflows/root-catalog.yml b/.github/workflows/root-catalog.yml new file mode 100644 index 0000000..a447331 --- /dev/null +++ b/.github/workflows/root-catalog.yml @@ -0,0 +1,23 @@ +name: Root README catalog + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + root-catalog: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Check root README against the marketplace catalog + run: node --test --test-name-pattern='^root catalog:' test/production-links.test.mjs diff --git a/README.md b/README.md index e2d3b35..73f3575 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ copilot plugin install azure-functions-hosted-skills@azure-dev-tools copilot plugin install azure-resources-query@azure-dev-tools copilot plugin install azure-cost-health-check@azure-dev-tools copilot plugin install azure-sre-agent@azure-dev-tools -copilot plugin install azure-storage-canvas@azure-dev-tools +copilot plugin install azure-storage@azure-dev-tools copilot plugin install containers@azure-dev-tools copilot plugin install canvas-authoring@azure-dev-tools ``` @@ -29,18 +29,18 @@ gear to add `microsoft/azure-dev-tools` (marketplace ID `azure-dev-tools`), and select a plugin. Fully quit and reopen GitHub Copilot after installation, then start a fresh chat. -The marketplace follows this repository's default branch. Immutable tags are -the reproducible install points: - -| Plugin | What it does | Production status | -| --- | --- | --- | -| [Azure Functions Hosted Skills](canvases/azure-functions-hosted-skills/) | Build and run local Hosted Skills or invoke supported functions in an existing Function App. | 0.5.5 is tagged; `azure-functions-hosted-skills-latest` points to that release. | -| [Azure Resources Query](canvases/azure-resources-query/) | Find and inspect Azure resources with read-only Resource Graph queries. | 0.1.5 is tagged; `azure-resources-query-latest` points to that release. | -| [Canvas Authoring](plugins/canvas-authoring/) | Add toolkit setup and starter apps to the native `create-canvas` workflow. | 0.1.1 is tagged; `canvas-authoring-latest` points to that release. Skill-only plugin, with no canvas panel. | -| [Azure Cost Health Check](canvases/azure-cost-health-check/) | Review spend, forecasts, budgets, alerts, recommendations, and AI billing. | 0.4.5 is tagged; `azure-cost-health-check-latest` points to that release. | -| [Azure SRE Agent](canvases/azure-sre-agent/) | Connect to your SRE Agent, diagnose failing apps, and investigate incident threads. | 0.2.7 is tagged; `azure-sre-agent-latest` points to that release. | -| [Azure Storage canvas](canvases/azure-storage-canvas/) | Browse blobs, inspect statistics, download files, and plan storage transfers with Copilot. | 0.1.1; immutable and `azure-storage-canvas-latest` install links become usable after the reviewed merge and tag verification. | -| [Containers](canvases/containers/) | Browse and operate local Docker or Podman containers and images. | 0.1.1 candidate; immutable and `containers-latest` install links become usable after the reviewed merge and tag verification. | +The marketplace follows this repository's default branch. Choose a plugin by +what you want to do: + +| Plugin | What it does | +| --- | --- | +| [Azure Functions Hosted Skills](canvases/azure-functions-hosted-skills/) | Build and run local Hosted Skills or invoke supported functions in an existing Function App. | +| [Azure Resources Query](canvases/azure-resources-query/) | Find and inspect Azure resources with read-only Resource Graph queries. | +| [Canvas Authoring](plugins/canvas-authoring/) | Add toolkit setup and starter apps to the native `create-canvas` workflow. Skill-only plugin, with no canvas panel. | +| [Azure Cost Health Check](canvases/azure-cost-health-check/) | Review spend, forecasts, budgets, alerts, recommendations, and AI billing. | +| [Azure SRE Agent](canvases/azure-sre-agent/) | Connect to your SRE Agent, diagnose failing apps, and investigate incident threads. | +| [Azure Storage](canvases/azure-storage/) | Browse blobs, inspect statistics, download files, and plan storage transfers with Copilot. | +| [Containers](canvases/containers/) | Browse and operate local Docker or Podman containers and images. | Native App marketplace installation has not been verified for every product; a source package or direct CLI install alone does not establish that the diff --git a/docs/release-process.md b/docs/release-process.md index ba4c8d0..85a6a1c 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -84,7 +84,8 @@ checks for each new package tree. Release order: 1. Start with content cleared for public distribution in this repository. -2. Run the package's required tests and release checks. +2. Run the package's required tests and release checks, including the root + catalog check described below. 3. Produce an inventory of the package files and record their SHA-256 digests in `SHA256SUMS`. 4. Obtain explicit approval for the candidate and its customer-facing @@ -100,6 +101,23 @@ Release order: Never move `PRODUCT-latest` to an unmerged branch, candidate commit, or unapproved rebuild. +### Keeping the root catalog current + +Update authoritative marketplace metadata and product release documentation for +each release. Keep versions, immutable tags, and checksums in those release +records and product installation instructions, not in the root README. +Update the root README only when a product is added, renamed, or removed, or +its capabilities, install name, or product README path change. + +Run `node --test --test-name-pattern='^root catalog:' test/production-links.test.mjs`. +The `Root README catalog` workflow in `.github/workflows/root-catalog.yml` runs +this guard on every pull request to `main` and every push to `main`, including +product release changes. It checks current catalog identities, install commands, +product README paths, and the skill-only and native marketplace qualifications. +Version-only updates need no root edit. This guard is separate from package +integrity, tag verification, and required release approvals; it replaces none +of them. + For the 0.5.3 Hosted Skills and 0.1.3 Azure Resources Query candidates, the Agent Plugins v1 manifests, protected previews, namespaced extensions, and complete new in-package `SHA256SUMS` receipts are verified with diff --git a/test/production-links.test.mjs b/test/production-links.test.mjs index 49b1847..09f186f 100644 --- a/test/production-links.test.mjs +++ b/test/production-links.test.mjs @@ -28,29 +28,94 @@ test("customer installation docs identify the production repository, not public } }); -test("production catalog matches all installable plugins in order", () => { - const readme = readFileSync(new URL("README.md", root), "utf8"); - const rows = readme.split("\n").filter((line) => line.startsWith("| [")); - const products = { - "azure-functions-hosted-skills": ["Azure Functions Hosted Skills", "canvases/azure-functions-hosted-skills/"], - "azure-resources-query": ["Azure Resources Query", "canvases/azure-resources-query/"], - "canvas-authoring": ["Canvas Authoring", "plugins/canvas-authoring/"], - "azure-cost-health-check": ["Azure Cost Health Check", "canvases/azure-cost-health-check/"], - "azure-sre-agent": ["Azure SRE Agent", "canvases/azure-sre-agent/"], - "containers": ["Containers", "canvases/containers/"], - }; - assert.equal(rows.length, manifest.plugins.length); - for (const [index, { name, version }] of manifest.plugins.entries()) { - const [label, path] = products[name]; - assert.ok(rows[index].startsWith(`| [${label}](${path}) |`), `catalog order: ${label}`); - assert.ok(existsSync(new URL(path, root)), `${label}: missing package`); - assert.ok(rows[index].includes(version), `${label}: current catalog version`); +function verifyRootCatalog(readme, catalog) { + const rows = readme.split("\n").filter((line) => line.startsWith("|")); + assert.equal(rows.shift(), "| Plugin | What it does |"); + assert.equal(rows.shift(), "| --- | --- |"); + const paths = rows.map((row) => { + const match = /^\| \[([^\]]+)\]\(([^)]+)\) \| ([^|]+) \|$/.exec(row); + assert.ok(match, `root product row: ${row}`); + return match[2]; + }); + assert.equal(new Set(catalog.plugins.map(({ name }) => name)).size, + catalog.plugins.length, "unique catalog identities"); + assert.deepEqual(paths, catalog.plugins.map(({ source }) => `${source}/`), + "root product paths match the current catalog in order"); + const installs = [...readme.matchAll(/copilot plugin install (\S+)/g)] + .map((match) => match[1]).sort(); + assert.deepEqual(installs, catalog.plugins.map(({ name }) => `${name}@${catalog.name}`).sort(), + "root install commands match the current catalog exactly"); + for (const { source } of catalog.plugins) { + assert.ok(existsSync(new URL(`${source}/README.md`, root)), + `${source}: missing product README`); + } + const authoring = catalog.plugins.find(({ name }) => name === "canvas-authoring"); + assert.ok(authoring, "skill-only companion remains in the catalog"); + assert.match(rows[paths.indexOf(`${authoring.source}/`)], + /Skill-only plugin, with no canvas panel/); + assert.match(readme, /Native App marketplace installation has not been verified for every product/); + assert.match(readme, /a source package or direct CLI install alone does not establish that the\s+native listing is live/); + assert.match(readme, /Each product README lists its prerequisites, safe prompts, first-run workflow,\s+version-pinned installation options, and product-specific safety behavior/); + assert.doesNotMatch(readme, /\b\d+\.\d+\.\d+\b|-latest\b|-v\d|\b[0-9a-f]{7,64}\b/, + "release versions, tag aliases, and hashes belong in product release docs"); +} + +const rootReadme = readFileSync(new URL("README.md", root), "utf8"); + +test("root catalog: matches all current install names and product README paths", () => { + verifyRootCatalog(rootReadme, manifest); +}); + +test("root catalog: version-only updates need no root README edit", () => { + const bumped = structuredClone(manifest); + for (const plugin of bumped.plugins) plugin.version = "99.99.99"; + assert.doesNotThrow(() => verifyRootCatalog(rootReadme, bumped)); +}); + +test("root catalog: rejects added, removed, renamed, duplicate, or relocated catalog identities", () => { + for (const update of [ + (catalog) => catalog.plugins.push({ name: "new-plugin", source: "canvases/new-plugin" }), + (catalog) => catalog.plugins.pop(), + (catalog) => { catalog.plugins[0].name = "renamed-plugin"; }, + (catalog) => catalog.plugins.push(structuredClone(catalog.plugins[0])), + (catalog) => { catalog.plugins[0].source = "canvases/relocated-plugin"; }, + ]) { + const changed = structuredClone(manifest); + update(changed); + assert.throws(() => verifyRootCatalog(rootReadme, changed), + /root product paths|root install commands|unique catalog identities/); + } +}); + +test("root catalog: rejects missing, obsolete, extra, or incorrect README installs and paths", () => { + const row = rootReadme.split("\n").find((line) => + line.startsWith("| [Azure Storage]")); + for (const changed of [ + rootReadme.replace("copilot plugin install azure-storage@azure-dev-tools\n", ""), + rootReadme.replace("azure-storage@azure-dev-tools", "azure-storage-canvas@azure-dev-tools"), + rootReadme.replace("azure-storage@azure-dev-tools", "azure-storage@wrong-marketplace"), + `${rootReadme}\ncopilot plugin install extra-plugin@azure-dev-tools\n`, + rootReadme.replace(row, `${row}\n${row}`), + rootReadme.replace(`${row}\n`, ""), + rootReadme.replace("](canvases/azure-storage/)", "](canvases/azure-storage-canvas/)"), + ]) { + assert.throws(() => verifyRootCatalog(changed, manifest), + /root product paths|root install commands/); + } +}); + +test("root catalog: retains skill-only, pinned-install, and native marketplace qualifications", () => { + for (const changed of [ + rootReadme.replace("Skill-only plugin, with no canvas panel.", ""), + rootReadme.replace("Native App marketplace installation has not been verified for every product;", ""), + rootReadme.replace("version-pinned installation options", "installation options"), + `${rootReadme}\n0.4.1 is tagged; azure-sre-agent-latest points to that release.\n`, + `${rootReadme}\nSource revision: abcdef0\n`, + `${rootReadme}\nRelease commit: bc8c3349df123c56eea4a957f8d63d3a4a8fd6e4\n`, + `${rootReadme}\nSHA-256 checksum: ${"a".repeat(64)}\n`, + ]) { + assert.throws(() => verifyRootCatalog(changed, manifest), assert.AssertionError); } - assert.match(rows[2], /Skill-only plugin, with no canvas panel/); - assert.match(readme, /copilot plugin install azure-sre-agent@azure-dev-tools/); - assert.match(readme, /\[Azure SRE Agent\]\(canvases\/azure-sre-agent\/\)/); - assert.match(readme, /copilot plugin install containers@azure-dev-tools/); - assert.match(readme, /\[Containers\]\(canvases\/containers\/\)/); }); test("SRE package has its catalog identity, official preview logo and customer connection steps", () => {