Skip to content

Commit f9ccf0c

Browse files
committed
Guard npm release publishing behind exclusive cutover
Require an operator-confirmed cutover before publishing with channel tags; document retirement and draining of the old publisher and correct the recheck comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 23fc3b8d-7678-46d3-9fe7-875d8311b526
1 parent 5360782 commit f9ccf0c

3 files changed

Lines changed: 22 additions & 3 deletions

File tree

‎.github/workflows/publish-npm.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ jobs:
2525
contents: read
2626
id-token: write
2727
steps:
28+
- name: Require completed npm publisher cutover
29+
env:
30+
CUTOVER_COMPLETE: ${{ vars.CLI_NPM_RELEASE_CUTOVER_COMPLETE }}
31+
run: |
32+
if [ "$CUTOVER_COMPLETE" != "true" ]; then
33+
echo "Set CLI_NPM_RELEASE_CUTOVER_COMPLETE only after retiring and draining the runtime npm publisher." >&2
34+
exit 1
35+
fi
2836
# Never check out the release tag: it can contain different workflow/script code.
2937
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
3038
with:

‎README.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ version-specific `release-<version>` npm tag if `latest` or `prerelease` has
113113
advanced, so recovery never intentionally downgrades those channels. A version
114114
already on npm with a missing/stale channel tag fails closed: npm OIDC cannot
115115
perform `npm dist-tag add`, so an npm administrator must repair that tag
116-
separately.
116+
separately. The npm dist-tag recheck cannot prevent a concurrent publisher
117+
from advancing a tag between the read and `npm publish --tag`; the cutover
118+
requires exclusive ownership of these packages' channel tags.
117119

118120
**Required setup before cutover:** On npmjs.com, configure an npm trusted
119121
publisher **with `npm publish` permission** for each of the nine packages:
@@ -127,7 +129,16 @@ repository is `github/copilot-cli`); leave environment unset. Use GitHub-hosted
127129
runners. The workflow uses Node 24, npm >= 11.5.1 and `id-token: write`, with
128130
no `NPM_TOKEN` or `NODE_AUTH_TOKEN`. The runtime repository must continue its
129131
existing publishing until this workflow is merged **and all nine npm trusted
130-
publishers are configured**; only then should its npm publication be cut over.
132+
publishers are configured**. At cutover, disable the old runtime
133+
`publish-cli.yml` workflow, wait for all its in-progress and queued runs to
134+
finish, then merge the runtime workflow change. Retire any other publisher
135+
of these nine packages and prohibit reruns of older runtime release runs.
136+
Only then set the `CLI_NPM_RELEASE_CUTOVER_COMPLETE` repository Actions
137+
variable to `true` in `github/copilot-cli` and re-enable the updated runtime
138+
workflow. Without this variable the new workflow fails before any npm
139+
publish, including manual recovery. If an external publisher is restarted,
140+
unset the variable before publishing another release; a dist-tag read is
141+
not a concurrency lock.
131142
Its internal Azure feed publication and ancillary release tasks remain separate.
132143
The release artifact producer must attach the nine actual npm package tarballs
133144
under the new `npm-github-copilot-` names before cutover. Older releases such as

‎script/publish-npm-release.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export async function publishRelease(tag, {
159159
console.log(`Already published ${item.name}@${version} (integrity matches)`);
160160
continue;
161161
}
162-
// Re-check immediately before publishing; a concurrent external publisher must not move a newer tag back.
162+
// Re-check for sequential changes; cross-repository publishers must be retired at cutover.
163163
const existing = await lookup(item.name, version);
164164
if (existing) {
165165
if (existing.dist?.integrity !== item.integrity) {

0 commit comments

Comments
 (0)