Skip to content

Commit 76e965e

Browse files
committed
fix(release): keep plugin tag mutation credential-isolated
1 parent 13c51a8 commit 76e965e

3 files changed

Lines changed: 24 additions & 50 deletions

File tree

.github/workflows/plugin-npm-release.yml

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ jobs:
339339
- name: Publish
340340
if: steps.npm_package_version.outputs.already_published != 'true'
341341
env:
342-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
343-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
342+
NODE_AUTH_TOKEN: ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
343+
NPM_TOKEN: ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
344344
OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher
345345
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
346346
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
@@ -351,46 +351,10 @@ jobs:
351351
PACKAGE_VERSION: ${{ matrix.plugin.version }}
352352
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
353353

354-
reconcile_plugins_npm:
354+
verify_plugins_npm:
355355
needs: [preview_plugins_npm, publish_plugins_npm]
356356
if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag == 'extended-stable' && needs.preview_plugins_npm.result == 'success' && (needs.publish_plugins_npm.result == 'success' || (needs.preview_plugins_npm.outputs.has_candidates == 'false' && needs.publish_plugins_npm.result == 'skipped')) }}
357357
runs-on: ubuntu-latest
358-
environment: npm-release
359-
permissions:
360-
contents: read
361-
strategy:
362-
fail-fast: false
363-
matrix:
364-
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.all_matrix) }}
365-
steps:
366-
- name: Reconcile extended-stable tag
367-
env:
368-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
369-
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
370-
PACKAGE_VERSION: ${{ matrix.plugin.version }}
371-
run: |
372-
set -euo pipefail
373-
exact_version="$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version 2>/dev/null || true)"
374-
if [[ "${exact_version}" != "${PACKAGE_VERSION}" ]]; then
375-
echo "Cannot reconcile ${PACKAGE_NAME}@extended-stable: ${PACKAGE_NAME}@${PACKAGE_VERSION} is not published." >&2
376-
exit 1
377-
fi
378-
tagged_version="$(npm view "${PACKAGE_NAME}@extended-stable" version 2>/dev/null || true)"
379-
if [[ "${tagged_version}" == "${PACKAGE_VERSION}" ]]; then
380-
echo "${PACKAGE_NAME}@extended-stable already points to ${PACKAGE_VERSION}."
381-
exit 0
382-
fi
383-
userconfig="$(mktemp)"
384-
trap 'rm -f "${userconfig}"' EXIT
385-
chmod 0600 "${userconfig}"
386-
printf '//registry.npmjs.org/:_authToken=%s\n' "${NPM_TOKEN}" > "${userconfig}"
387-
NPM_CONFIG_USERCONFIG="${userconfig}" npm dist-tag add "${PACKAGE_NAME}@${PACKAGE_VERSION}" extended-stable
388-
echo "Reconciled ${PACKAGE_NAME}@extended-stable from ${tagged_version:-<missing>} to ${PACKAGE_VERSION}."
389-
390-
verify_plugins_npm:
391-
needs: [preview_plugins_npm, reconcile_plugins_npm]
392-
if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag == 'extended-stable' && needs.preview_plugins_npm.result == 'success' && needs.reconcile_plugins_npm.result == 'success' }}
393-
runs-on: ubuntu-latest
394358
permissions:
395359
contents: read
396360
strategy:
@@ -431,4 +395,5 @@ jobs:
431395
fi
432396
done
433397
echo "npm registry did not converge for ${PACKAGE_NAME}@${PACKAGE_VERSION} (exact=${exact_version:-missing}, extended-stable=${tagged_version:-missing})." >&2
398+
echo "This OIDC-only source workflow does not mutate tags on an already-published package; use credential-isolated release tooling for manual tag repair." >&2
434399
exit 1

docs/reference/RELEASING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,11 @@ behavior or `npm_dist_tag=extended-stable` for the guarded monthly path. The
506506
extended-stable option requires `publish_scope=all-publishable`, an empty
507507
`plugins` input, a final patch at or above `33`, and the canonical
508508
`extended-stable/YYYY.M.33` branch at its exact tip. It never moves plugin
509-
`latest` or `beta`.
509+
`latest` or `beta`. New package versions receive `extended-stable` atomically
510+
through OIDC trusted publication (`npm publish --tag extended-stable`); this
511+
source workflow does not use token-authenticated `npm dist-tag add`. Retries
512+
skip exact versions already present in npm, then fail closed unless complete
513+
readback confirms that every exact package and `extended-stable` tag converged.
510514

511515
`OpenClaw Release Publish` accepts these operator-controlled inputs:
512516

test/scripts/plugin-npm-extended-stable-workflow.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,24 +81,29 @@ describe("plugin npm extended-stable workflow", () => {
8181
);
8282
});
8383

84-
it("repairs every publishable extended-stable tag before final readback", () => {
84+
it("publishes extended-stable with OIDC only and verifies every package tag", () => {
8585
const parsed = workflow();
86-
const reconcile = parsed.jobs?.reconcile_plugins_npm;
87-
expect(reconcile?.needs).toEqual(["preview_plugins_npm", "publish_plugins_npm"]);
88-
expect(reconcile?.environment).toBe("npm-release");
89-
expect(reconcile?.if).toContain("has_candidates == 'false'");
90-
expect(reconcile?.strategy?.matrix?.plugin).toContain("all_matrix");
91-
const repair = step(reconcile, "Reconcile extended-stable tag");
92-
expect(repair.run).toContain(
86+
const publish = step(parsed.jobs?.publish_plugins_npm, "Publish");
87+
const tokenExpression =
88+
"${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}";
89+
expect(publish.env).toMatchObject({
90+
NODE_AUTH_TOKEN: tokenExpression,
91+
NPM_TOKEN: tokenExpression,
92+
OPENCLAW_NPM_PUBLISH_AUTH_MODE: "trusted-publisher",
93+
});
94+
expect(parsed.jobs?.reconcile_plugins_npm).toBeUndefined();
95+
expect(readFileSync(workflowPath, "utf8")).not.toContain(
9396
'npm dist-tag add "${PACKAGE_NAME}@${PACKAGE_VERSION}" extended-stable',
9497
);
95-
expect(repair.run).toContain('NPM_CONFIG_USERCONFIG="${userconfig}"');
9698

9799
const verify = parsed.jobs?.verify_plugins_npm;
98-
expect(verify?.needs).toEqual(["preview_plugins_npm", "reconcile_plugins_npm"]);
100+
expect(verify?.needs).toEqual(["preview_plugins_npm", "publish_plugins_npm"]);
99101
expect(verify?.if).toContain("always()");
102+
expect(verify?.if).toContain("has_candidates == 'false'");
103+
expect(verify?.strategy?.matrix?.plugin).toContain("all_matrix");
100104
const readback = step(verify, "Verify complete plugin registry readback");
101105
expect(readback.run).toContain('npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version');
102106
expect(readback.run).toContain('npm view "${PACKAGE_NAME}@extended-stable" version');
107+
expect(readback.run).toContain("OIDC-only source workflow does not mutate tags");
103108
});
104109
});

0 commit comments

Comments
 (0)