diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e10fc6206..510b6109d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -213,7 +213,8 @@ jobs: if [ "${published_version}" = "\"${version}\"" ]; then echo "rea-agents@${version} is already published; verifying the existing package." else - npm publish --access public + tag="$(node scripts/release-npm-tag.mjs "${version}")" + npm publish --access public --tag "${tag}" fi - name: Verify published CLI from npm diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 7445df1e6..3d18c2033 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -3,8 +3,8 @@ ## Generated catalog Run `npm run build:cached` in a source checkout to generate the machine-readable -catalog at `docs/public/product-catalog.json`. The [catalog](product-catalog.json) -is also included in documentation builds. PR CI retains it with the packaged +catalog at `docs/public/product-catalog.json`. Documentation deployments serve +the same file at [/rea/product-catalog.json](/rea/product-catalog.json). PR CI retains it with the packaged skill and portable conformance projections in the `generated-docs` artifact. These outputs describe the exact source revision being built; they are not checked-in snapshots. For a running server, `binary_session` remains the diff --git a/docs/releasing.md b/docs/releasing.md index 56fc66264..a8859808d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -115,6 +115,8 @@ gh workflow run release.yml --ref release/5.0.0 \ Publication creates the release from the merged bot PR without preparing or updating another PR. Both npm and MCP Registry jobs check out Release Please's exact release SHA. They do not build the current main tip or a mutable branch. +Stable versions publish to npm's `latest` tag; prerelease versions publish to +the `next` tag so they cannot replace the stable install by default. The publish dispatch runs from the frozen release branch so npm's provenance records the actual release commit. Before Release Please creates a tag, the workflow accepts only `prepare` or `publish` and requires the selected branch diff --git a/scripts/release-npm-tag.mjs b/scripts/release-npm-tag.mjs new file mode 100644 index 000000000..37766a4ec --- /dev/null +++ b/scripts/release-npm-tag.mjs @@ -0,0 +1,9 @@ +import semver from "semver"; + +const version = process.argv[2]; +if (version === undefined || semver.valid(version) !== version) { + console.error("Expected an exact SemVer version."); + process.exitCode = 1; +} else { + process.stdout.write(semver.prerelease(version) === null ? "latest" : "next"); +} diff --git a/tests/boundary/filesystem/releaseWorkflow.test.ts b/tests/boundary/filesystem/releaseWorkflow.test.ts index c8d404ef1..bb0e86b78 100644 --- a/tests/boundary/filesystem/releaseWorkflow.test.ts +++ b/tests/boundary/filesystem/releaseWorkflow.test.ts @@ -213,6 +213,27 @@ it("binds npm and MCP publication to the same immutable release SHA", async () = "inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'", ); } + const publishCommand = workflow.jobs.publish.steps.find( + (step) => step.name === "Publish", + )?.run; + expect(publishCommand).toContain("scripts/release-npm-tag.mjs"); + expect(publishCommand).toContain( + 'npm publish --access public --tag "${tag}"', + ); + for (const [version, tag] of [ + ["6.1.0", "latest"], + ["6.1.0-rc.1", "next"], + ] as const) { + const helper = new URL( + "../../../scripts/release-npm-tag.mjs", + import.meta.url, + ); + const result = await execFileAsync(process.execPath, [ + helper.pathname, + version, + ]); + expect(result.stdout).toBe(tag); + } }); it