diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index 5089b452e220..7992881dbe2f 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -1241,8 +1241,55 @@ jobs: PACKAGE_VERSION: ${{ matrix.plugin.version }} run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}" - - name: Publish approved bootstrap tarball + - name: Check bootstrap npm package version + id: bootstrap_npm_package_version if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' + env: + PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }} + PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }} + TARBALL_PATH: ${{ steps.publication_evidence.outputs.tarball_path }} + run: | + set -euo pipefail + node --input-type=module <<'NODE' >> "$GITHUB_OUTPUT" + import crypto from "node:crypto"; + import fs from "node:fs"; + import { fetchNpmRegistryPackumentWithRetry } from "./scripts/lib/npm-publish-plan.mjs"; + + const packageName = process.env.PACKAGE_NAME; + const packageVersion = process.env.PACKAGE_VERSION; + const tarball = fs.readFileSync(process.env.TARBALL_PATH); + const expectedIntegrity = `sha512-${crypto.createHash("sha512").update(tarball).digest("base64")}`; + const expectedShasum = crypto.createHash("sha1").update(tarball).digest("hex"); + const registryFetch = await fetchNpmRegistryPackumentWithRetry({ + packageName, + packageUrl: `https://registry.npmjs.org/${encodeURIComponent(packageName)}`, + }); + if (registryFetch.status === 404) { + console.log("already_published=false"); + process.exit(0); + } + if (!registryFetch.ok) { + throw new Error(`${packageName}: npm registry returned HTTP ${registryFetch.status}.`); + } + const publishedDist = registryFetch.packument?.versions?.[packageVersion]?.dist; + if (!publishedDist) { + console.log("already_published=false"); + process.exit(0); + } + if ( + publishedDist.integrity !== expectedIntegrity || + publishedDist.shasum !== expectedShasum + ) { + throw new Error( + `${packageName}@${packageVersion}: npm registry tarball identity does not match the approved bootstrap artifact.`, + ); + } + console.error(`${packageName}@${packageVersion} already matches the approved bootstrap artifact.`); + console.log("already_published=true"); + NODE + + - name: Publish approved bootstrap tarball + if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' && steps.bootstrap_npm_package_version.outputs.already_published != 'true' env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} PACKAGE_DIR: ${{ matrix.plugin.packageDir }} @@ -1301,12 +1348,17 @@ jobs: - name: Record Meta trusted publisher checkpoint if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' env: + ALREADY_PUBLISHED: ${{ steps.bootstrap_npm_package_version.outputs.already_published }} PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }} run: | { echo "## Meta npm bootstrap follow-up" echo - echo "- Published \`${PACKAGE_NAME}\` from the verified immutable tarball." + if [[ "$ALREADY_PUBLISHED" == "true" ]]; then + echo "- Verified existing \`${PACKAGE_NAME}\` against the approved immutable tarball." + else + echo "- Published \`${PACKAGE_NAME}\` from the verified immutable tarball." + fi echo "- Configure the GitHub trusted publisher for \`plugin-npm-release.yml\` and environment \`npm-release\` before the next OIDC publish." } >> "$GITHUB_STEP_SUMMARY" diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index a06545364738..9c6d39b77be3 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -329,8 +329,19 @@ describe("plugin npm extended-stable workflow", () => { }); expect(publish.env?.NODE_AUTH_TOKEN).toBeUndefined(); expect(publish.env?.NPM_TOKEN).toBeUndefined(); + const bootstrapCheck = step( + parsed.jobs?.publish_plugins_npm, + "Check bootstrap npm package version", + ); + expect(bootstrapCheck.if).toContain("npm-token-bootstrap"); + expect(bootstrapCheck.run).toContain("fetchNpmRegistryPackumentWithRetry"); + expect(bootstrapCheck.run).toContain("publishedDist.integrity !== expectedIntegrity"); + expect(bootstrapCheck.run).toContain("already_published=true"); const bootstrap = step(parsed.jobs?.publish_plugins_npm, "Publish approved bootstrap tarball"); expect(bootstrap.if).toContain("npm-token-bootstrap"); + expect(bootstrap.if).toContain( + "steps.bootstrap_npm_package_version.outputs.already_published != 'true'", + ); expect(bootstrap.env?.NPM_TOKEN).toBe("${{ secrets.NPM_TOKEN }}"); expect(bootstrap.env?.PACKAGE_NAME).toContain("publication_evidence.outputs.package_name"); expect(bootstrap.run).not.toContain("@openclaw/meta-provider");