diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index e4f24f0..9e2c67f 100755 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -9,6 +9,10 @@ on: version: description: 'Version already committed in js/package.json (e.g. 2.8.0)' required: true + verify_only: + description: 'Verify an already-published version without publishing again' + type: boolean + default: false permissions: contents: read @@ -55,18 +59,23 @@ jobs: # npm 11.21 supports private dist-tag reads through trusted publishing. - run: npm install --global npm@11.21.0 - name: Check npm publishing access - run: npm dist-tag ls @dev.smartpricing/traceflow-sdk --json + run: npm dist-tag ls @dev.smartpricing/traceflow-sdk env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN || secrets.NO_TWOFA_NPM_TOKEN }} # Kafka is needed only for its declarations; do not build its native addon. - run: npm ci --ignore-scripts + if: inputs.verify_only != true - run: npm run typecheck:errors && npm run test:release && npm test -- --run + if: inputs.verify_only != true - run: npm run build + if: inputs.verify_only != true - name: Verify the npm archive + if: inputs.verify_only != true run: npm run test:package -- "$RUNNER_TEMP/traceflow-sdk.tgz" - name: Publish to npm + if: inputs.verify_only != true run: npm publish "$RUNNER_TEMP/traceflow-sdk.tgz" --tag latest env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN || secrets.NO_TWOFA_NPM_TOKEN }} @@ -80,10 +89,11 @@ jobs: import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { appendFileSync, readFileSync } from 'node:fs'; + import { readLatestVersion } from './scripts/release-version.mjs'; const pkg = JSON.parse(readFileSync('package.json', 'utf8')); - const tags = JSON.parse(execFileSync('npm', ['dist-tag', 'ls', pkg.name, '--json'], { encoding: 'utf8' })); - assert.equal(tags.latest, pkg.version); - const message = `Published ${pkg.name}@${pkg.version}; npm latest verified. The published archive passed export, CJS/ESM import, and frontend bundle checks.`; + const tags = execFileSync('npm', ['dist-tag', 'ls', pkg.name], { encoding: 'utf8' }); + assert.equal(readLatestVersion(tags), pkg.version); + const message = `Verified npm latest: ${pkg.name}@${pkg.version}.`; console.log(message); appendFileSync(process.env.GITHUB_STEP_SUMMARY, message + '\n'); NODE diff --git a/js/README.md b/js/README.md index e3030d2..5989b2f 100644 --- a/js/README.md +++ b/js/README.md @@ -203,6 +203,13 @@ For recovery after a failed publish, run the workflow with the committed version gh workflow run workflow.yaml --ref main -f version=2.8.0 ``` +If publishing succeeded and only the final check failed, verify the existing +release without attempting to publish the same version again: + +```sh +gh workflow run workflow.yaml --ref main -f version=2.8.0 -f verify_only=true +``` + The workflow tests and builds every entry point, installs and verifies the npm archive, then publishes that same archive. It verifies npm's `latest` tag after publishing. The existing `release/X.Y.Z` flow is also supported: commit version diff --git a/js/scripts/release-version.check.mjs b/js/scripts/release-version.check.mjs index fee9bd1..f442851 100644 --- a/js/scripts/release-version.check.mjs +++ b/js/scripts/release-version.check.mjs @@ -1,6 +1,20 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { shouldRelease } from "./release-version.mjs"; +import { readLatestVersion, shouldRelease } from "./release-version.mjs"; + +test("read the plain-text output returned by npm dist-tag ls", () => { + assert.equal(readLatestVersion("latest: 2.8.0\n"), "2.8.0"); +}); + +test("select latest when the registry also contains other tags", () => { + assert.equal(readLatestVersion("beta: 3.0.0-beta.1\nlatest: 2.8.0\nnext: 2.9.0\n"), "2.8.0"); +}); + +test("reject missing or invalid latest tags", () => { + for (const output of ["", "next: 2.8.0\n", "latest: \nnext: 2.8.0\n", "latest: invalid\n"]) { + assert.throws(() => readLatestVersion(output)); + } +}); const main = { event: "push", branch: "main", version: "2.8.0", previous: "2.8.0" }; diff --git a/js/scripts/release-version.mjs b/js/scripts/release-version.mjs index ba6b953..85ae29e 100644 --- a/js/scripts/release-version.mjs +++ b/js/scripts/release-version.mjs @@ -6,6 +6,14 @@ import { fileURLToPath } from "node:url"; const stableVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; +export function readLatestVersion(output) { + // npm dist-tag ls emits text even when passed --json. + const match = output.match(/^latest:[ \t]+(\S+)[ \t]*$/m); + assert(match, "npm did not return a latest tag"); + assert.match(match[1], stableVersion, "npm latest must be a stable version"); + return match[1]; +} + export function shouldRelease({ event, branch, requested, version, previous }) { assert.match(version, stableVersion, "Releases must use a stable X.Y.Z version"); if (event === "workflow_dispatch") {