Repository navigation
[DO NOT MERGE] ci: stage npm releases via OIDC instead of publishing directly #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Replacement for `changeset publish` that uses npm staged publishing. | ||
| // | ||
| // `npm stage publish` uploads the tarball to the npm stage queue through the | ||
| // trusted publisher (OIDC). The version only goes live after a maintainer | ||
| // approves it with 2FA (`npm stage approve <stage-id>` or on npmjs.com). | ||
| // | ||
| // changesets/action detects a release in a single-package repo by looking for a | ||
| // "New tag:" line on stdout. It then pushes the `v<version>` tag and creates the | ||
| // GitHub release, so this script creates the tag locally and prints that line. | ||
|
|
||
| import { execFileSync } from "node:child_process"; | ||
| import { readFileSync } from "node:fs"; | ||
|
|
||
| const { name, version } = JSON.parse(readFileSync("package.json", "utf8")); | ||
| const tag = `v${version}`; | ||
|
|
||
| const run = (command, args, options = {}) => | ||
| (execFileSync(command, args, { encoding: "utf8", ...options }) ?? "").trim(); | ||
|
|
||
| const isPublished = () => { | ||
| try { | ||
| return run("npm", ["view", `${name}@${version}`, "version"]) === version; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| // The tag is pushed right after staging, so it also marks a version that is | ||
| // staged and still waiting for approval. | ||
| const isTagged = () => | ||
| run("git", ["ls-remote", "--tags", "origin", `refs/tags/${tag}`]) !== ""; | ||
|
|
||
| const log = (message) => process.stdout.write(`${message}\n`); | ||
|
|
||
| if (isPublished()) { | ||
| log(`${name}@${version} is already published, skipping.`); | ||
| } else if (isTagged()) { | ||
| log(`${tag} already exists, ${name}@${version} is staged, skipping.`); | ||
| } else { | ||
| run("npm", ["stage", "publish"], { stdio: "inherit" }); | ||
| run("git", ["tag", tag]); | ||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Recover a stage when tag publication fails. If staging succeeds but |
||
|
|
||
| log(`Staged ${name}@${version}, approve it with 2FA to publish.`); | ||
| log(`New tag: ${tag}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Delay the public release signal until approval. If a maintainer has not approved the stage, this |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Pass an npm dist-tag for prerelease versions.
If Changesets produces a version such as
5.1.0-beta.1, this command uses npm’s defaultlatesttag.npm stage publishrejects prerelease versions without an explicit tag. Select the Changesets prerelease tag and pass--tagwhen staging that version. Otherwise, the prerelease workflow stops before it creates a stage. (docs.npmjs.com)