feat: automate verified npm and GitHub releases - #91
Conversation
|
@codex review Please focus on release trigger correctness, GitHub Actions permission and trust boundaries, npm provenance and exact gitHead verification, idempotent historical backfills, latest-release selection, and automatic release-note category reconciliation. |
There was a problem hiding this comment.
Pull request overview
This PR introduces an automated, verification-driven release pipeline that (1) cuts stable npm-vX.Y.Z tags and dispatches the protected npm publishing workflow, then (2) publishes a GitHub Release only after validating npm publication metadata, gitHead, SLSA provenance, and tarball integrity, with automated release-note labeling and changelog configuration.
Changes:
- Add a release automation Node script plus Bun tests to validate stable versioning, npm publication verification, and idempotent GitHub Release verification.
- Introduce GitHub Actions workflows to cut tags, dispatch protected npm publishing, and publish verified GitHub Releases with generated notes (without forcing “latest” on historical backfills).
- Add automated PR labeling for release-note categories and configure GitHub Release note categories/exclusions.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/typescript/tests-ts/release-automation.test.ts | Adds Bun tests covering version validation, npm metadata/artifact verification, and workflow safeguard assertions. |
| sdk/typescript/scripts/release-automation.mjs | Implements release version parsing plus npm/GitHub Release verification helpers and CLI entrypoints. |
| .github/workflows/node-release.yml | Enables manual dispatch for the existing protected npm publishing workflow. |
| .github/workflows/node-release-labels.yml | Adds a pull_request_target workflow that auto-labels PRs for release-note categorization. |
| .github/workflows/node-release-cut.yml | Adds a workflow to cut npm-vX.Y.Z tags from merged stable bumps and dispatch the protected publisher. |
| .github/workflows/node-github-release.yml | Adds a workflow that publishes GitHub Releases only after verifying successful npm publication and artifact integrity. |
| .github/release.yml | Configures release-note categories and excludes internal changes via labels. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 335411fc07
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review All prior findings are fixed and individually resolved on 35413aa. Please re-review tag-only publishing, main-only release cuts, strictly increasing versions, historical/latest release handling, concurrent PR labeling, durable tarball backfill, and cryptographically verified npm/SLSA provenance. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
sdk/typescript/scripts/release-automation.mjs:217
verified.attestationBundles?.find(...)can throw a TypeError ifattestationBundlesis present but not an array (untrusted JSON fromnpm audit signatures). This bypasses the intended controlled error path ("The verified SLSA provenance bundle is missing.") and can make failures harder to diagnose. Guard withArray.isArraybefore calling.find.
const provenance = verified.attestationBundles?.find(
(candidate) => candidate?.predicateType === provenancePredicate,
);
sdk/typescript/scripts/release-automation.mjs:332
- The
.findcallback assumes every entry inrelease.assetsis an object and unconditionally readscandidate.name. If the API response contains an unexpected/null entry, this will throw a TypeError instead of producing the intended verification error. Using optional chaining keeps behavior deterministic with untrusted JSON.
const asset = Array.isArray(release.assets)
? release.assets.find((candidate) => candidate.name === assetName)
: undefined;
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35413aafa3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review All 16 prior review threads are fixed, regression-tested, individually answered, and resolved on bf19d80. Please recheck the durable GitHub Actions release queues, monotonic manual release cuts, repository-wide Latest serialization, pinned Node/npm attestation support, full Fulcio/OIDC certificate identity verification, protected npm environment, and idempotent concurrent label creation. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
.github/workflows/node-github-release.yml:83
- The workflow claims to require an existing release tag, but it only validates the tag shape and then resolves
release_shavia thecommits/<ref>endpoint, which can also resolve branch names. Add an explicit check thatrefs/tags/$release_tagexists before proceeding so manual dispatches can’t accidentally accept a branch ref and so missing tags fail with the intended error message.
if [[ ! "$release_tag" =~ ^npm-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "GitHub releases require an existing stable npm-vX.Y.Z tag." >&2
exit 1
fi
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf19d80ab7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review All 17 prior findings are regression-tested, replied to, and resolved on 65c0722. Please verify direct publisher downgrade protection, exact Git-tag resolution, durable release queues, verified Fulcio certificate identity, historical release handling, and concurrent release-note labels. |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
.github/workflows/node-github-release.yml:90
release_shais resolved viarepos/.../commits/$release_tag, which can be ambiguous if a branch with the same name exists. Since the workflow already requires a real tag, resolve the commit SHA from thegit/ref/tags/...object (and dereference annotated tags) so the commit cannot be influenced by a same-named branch.
if ! gh api \
"repos/$GITHUB_REPOSITORY/git/ref/tags/$release_tag" \
--silent >/dev/null 2>&1; then
echo "GitHub releases require an existing stable npm-vX.Y.Z tag." >&2
exit 1
fi
release_sha="$(
gh api "repos/$GITHUB_REPOSITORY/commits/$release_tag" \
--jq '.sha'
)"
|
@codex review All 17 prior findings are fixed, regression-tested, individually replied to, and resolved on 75496c2. This head is synchronized to main v0.1.3. It also addresses Copilot's latest suppressed finding: release commits now come from the exact real Git tag-object SHA, with passing regressions for lightweight tags, annotated tags, and same-named branches. Please review final tag resolution, direct publisher monotonicity, durable release queues, Fulcio/OIDC certificate identity, historical release notes, and concurrent labels. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c93c564b1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Please review exact final head |
…te-github-npm-releases-20260729
|
@codex review Please review exact current head |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
.github/workflows/node-release.yml:233
- PR description states Node.js 24.15.0 is pinned in both the release cutter and the protected publisher, but this workflow only pins 24.15.0 in the protected
publishjob while theverifyjob still uses Node.js 22. Either update the description or pin the same Node version inverifyas well to keep the release pipeline consistent.
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: "24.15.0"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false
.github/workflows/node-github-release.yml:508
- The Latest-release lookup treats a missing Latest as acceptable only when the JSON body contains
status: "404".gh api repos/.../releases/latesttypically returns an error body without astatusfield, so the workflow would fail closed even when there is simply no current Latest release (e.g., early in repo history), preventing the intended reconciliation behavior.
} else if (String(response?.status) !== "404") {
throw new Error(
"Unable to resolve the current Latest GitHub Release.",
);
}
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review exact final head |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review exact final head |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
.github/workflows/node-github-release.yml:435
- The existing-release check treats any
gh apifailure as “release does not exist” because stderr is silenced and the HTTP status is not inspected. This means transient/network/5xx/auth failures can fall through into the create-release path instead of failing closed; only a genuine 404 should be treated as “missing”.
if existing_release="$(
gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" \
2>/dev/null
)"; then
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review exact final head |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
sdk/typescript/tests-ts/release-automation.test.ts:1296
- This describe block executes GitHub Actions workflow steps by spawning
bash. Since the repo’s CI runs the Bun test suite onwindows-latest(see.github/workflows/node-ci.yml), adding an unconditionalbashdependency here makes the test suite rely on Git-for-Windows being present in PATH and makes local Windows runs fragile. Consider skipping the workflow-shell execution tests on Windows (the workflows themselves run on Ubuntu) to keep the test suite portable.
describe("GitHub release workflow safeguards", () => {
test("requires a real tag for protected npm publication", () => {
expect(protectedReleaseWorkflow).toContain("release-tag");
expect(protectedReleaseWorkflow).toContain('"$GITHUB_REF_TYPE"');
expect(protectedReleaseWorkflow).toContain('"$GITHUB_REF"');
});
|
Copilot follow-up on exact head |
Summary
maininto its exactnpm-vX.Y.Ztag and dispatch the existing protected npm publisher.main, and either an increasing stable npm version or an authenticated recovery of the exact highest already-published version. Treat an npmE404as empty history only for the initial0.1.0release; fail closed on later releases and all other registry/auth/network errors.queue: max, preserving intermediate versions and preventing historical releases from racing to become Latest.refs/tags/npm-vX.Y.Zobject, correctly peel lightweight and annotated tags through the GitHub Git Tags API without assuming tag objects exist locally, and never resolve a same-named branch.gitHead, SHA-512 package integrity, registry signatures, and Sigstore/SLSA provenance.npmenvironment.22.13.0and the release cutter, protected publisher, and GitHub Release job to Node.js24.15.0; install npm11.12.1into an isolated runner-temporary prefix through the Socket Firewall; and verify the exact version plusnpm audit signatures --json --include-attestations.404responses as missing even when their JSON body omitsstatus, and fail closed on authentication errors, malformed responses, network failures, and all other HTTP errors before release creation or reconciliation, reconcile incorrect Latest metadata even when existing notes are current, safely backfill expired Actions artifacts, and cryptographically verify existing GitHub assets even when their API digests are absent.skip-release-noteslabels by checking the true latest labeling event, while still reconciling other managed category labels when a PR title changes and avoiding redundant label writes.mainbranch and explicitly check outrefs/heads/mainbefore running contents-write automation.npm-v0.1.2release from protected run30481596229without republishing npm.maincommit9c7634b5d08b36e6060441fe3e35caaaf8aa6a49, including the0.1.4package, nested Git repository snapshots/remediation checkouts, and latest scan-comparison CLI/workbench changes, without rewriting the existing PR branch.Why
Tags created using
GITHUB_TOKENdo not start a second tag-push workflow. The cutter explicitly dispatches the existing protected publisher on the true release tag; a separately retryable GitHub Release workflow then publishes generated release notes only after authenticating the public npm artifact and its protected provenance.GitHub's documented durable concurrency queues retain pending releases instead of replacing the previous pending run. A repository-wide GitHub Release queue also serializes Latest selection and release creation across versions.
Review feedback addressed
Validation
6820cc44cd80b95ac1192f1d73bbd9a144f8add5; mergedmain:9c7634b5d08b36e6060441fe3e35caaaf8aa6a49.E404handling with fail-closed auth/network/later-version errors, stale/empty/current generated release notes, missing-release and missing-Latest HTTP404responses without a JSONstatus; fail-closed existing-release HTTP500, HTTP401, malformed404, and network failures; and fail-closed Latest HTTP500, malformed404, and network failures, Latest-status repair, manually skipped category-label reconciliation, unavailable annotated objects, tag deletion/retargeting immediately before release creation or edit, unattributed latest label events, and GitHub CLI edit compatibility.0.1.2archive against npm integrity, source commit1e03c89ad22d2df5ae65b146be1483b3608572a9, protected Actions run30481596229, Fulcio certificate, exact workflow/tag identity, and protectednpmenvironment.0.1.4tarball withgitHeadbound to exact final commit6820cc44cd80b95ac1192f1d73bbd9a144f8add5; independently validated all 179 archive entries, public SDK import, executable CLI, and all 95 bundled plugin files.actionlint1.7.12passes with only a narrowly scoped compatibility exclusion for its outdated rejection of GitHub's officially documentedconcurrency.queue: maxsyntax.