ci: hardened changesets release workflow (OIDC, no stored npm token)#57
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a hardened GitHub Actions release workflow for this repo that (1) continuously opens/updates the Changesets “Version Packages” PR on pushes to main, and (2) publishes @taskless/cli to npm via GitHub OIDC trusted publishing when the version bump lands on main, avoiding any stored long-lived npm token.
Changes:
- Introduces
.github/workflows/release.ymlwith a two-job model: uncredentialedversion(creates/updates Version Packages PR) and credentialedpublish(OIDC-based npm publish gated on version-not-yet-on-npm). - Implements least-privilege permissions per job and adds release serialization via workflow-level
concurrency. - Adds a publish gate that checks npm for the exact package version before building/publishing.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
thecodedrift
added a commit
that referenced
this pull request
Jul 10, 2026
Copilot review on #57: - node-version 20 → 24: @taskless/cli requires engines.node >=22.22.0 and CI runs Node 24, so the release jobs would have failed at install/build. - Pin npm@12.0.1 instead of npm@latest so the release is deterministic and a new npm release can't change publish behavior unreviewed (still satisfies the OIDC >= 11.5.1 requirement). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thecodedrift
marked this pull request as ready for review
July 10, 2026 19:17
Automate the changesets release: a `version` job opens the Version
Packages PR from pending changesets, and a `publish` job publishes to npm
when main's version isn't yet published.
Hardening:
- Split jobs: `version` has no credential and only handles untrusted
changeset text (into CHANGELOG/PR body); `publish` sees no untrusted
text and builds only reviewed, merged source.
- npm OIDC trusted publishing — short-lived token, no stored NPM_TOKEN.
- Least privilege: version = contents/pull-requests write; publish =
contents:read + id-token:write, scoped to an `npm-production`
environment. No approver gate (automatic on Version PR merge).
- --ignore-scripts, explicit build, --provenance, pinned action SHAs,
push:main only (no pull_request_target), no ${{ }} run: interpolation.
Draft: requires the npm trusted-publisher + the `npm-production`
environment to be configured before the publish path can succeed (see PR
description). Publish is version-gated, so merging the workflow itself
never publishes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot review on #57: - node-version 20 → 24: @taskless/cli requires engines.node >=22.22.0 and CI runs Node 24, so the release jobs would have failed at install/build. - Pin npm@12.0.1 instead of npm@latest so the release is deterministic and a new npm release can't change publish behavior unreviewed (still satisfies the OIDC >= 11.5.1 requirement). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thecodedrift
force-pushed
the
jakob/release-workflow
branch
from
July 10, 2026 20:01
9db1456 to
1356571
Compare
Copilot review (under the new .github/copilot-instructions.md): the publish job granted id-token: write and bound the npm-production environment on every push to main, even when the version check decided there was nothing to publish — needlessly widening the window an OIDC identity exists. Split the decision out: a credential-free `check` job (contents:read only) computes publish=true|false; `publish` now `needs: check` and runs only `if: needs.check.outputs.publish == 'true'`. On ordinary pushes the OIDC-capable job never starts, so the release identity + environment exist only for an actual release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two more defense-in-depth findings from the Copilot re-review under the new GHA credential-safety instructions: - persist-credentials: false on the check and publish checkouts — those jobs don't push, so don't leave GITHUB_TOKEN in the local git config (smaller blast radius if a later step uses git/network). - --ignore-scripts on the pinned `npm install -g` so no dependency lifecycle code executes while the publish job holds id-token: write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thecodedrift
added a commit
that referenced
this pull request
Jul 10, 2026
Copilot review on #57: - node-version 20 → 24: @taskless/cli requires engines.node >=22.22.0 and CI runs Node 24, so the release jobs would have failed at install/build. - Pin npm@12.0.1 instead of npm@latest so the release is deterministic and a new npm release can't change publish behavior unreviewed (still satisfies the OIDC >= 11.5.1 requirement). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automates changeset releases so the "Version Packages" PR is rebuilt as work lands on
main, and@taskless/clipublishes to npm when that PR merges — without a long-lived npm token. Adapted from the pattern inthecodedrift/firebot-script-music-to-my-earsand hardened for npm publishing. MIT licensed workSecurity model
Two jobs, split by credential exposure:
version— reads contributor-authored changesets (untrusted) and opens the Version Packages PR. No npm credential, no OIDC identity. The changeset text is fully consumed here (CHANGELOG + PR body) and never reaches the credentialed job, so a crafted changeset/PR body has nothing to steal or escape into.publish— runs only whenmain's version isn't on npm (i.e. right after the Version PR merges). By then there are no changesets left, so it sees no untrusted text and builds only reviewed, merged source. Auth is a short-lived token minted via GitHub OIDC (npm trusted publishing) — no storedNPM_TOKENanywhere.Also: least-privilege per job,
--ignore-scripts+ explicit build,--provenance, action SHAs pinned,push: mainonly (neverpull_request_target), and no${{ }}interpolation of untrusted text into anyrun:.Residual perimeter, stated honestly: the publish job builds merged code, so what can merge to
mainis the real boundary — enforced by branch protection. Per discussion, we're relying on that (no approver gate) plus security-focused Copilot review rather than a manual environment gate.Required setup before this can publish (needs npm admin)
npm-production(repo Settings → Environments → New environment). No secrets needed; optionally restrict its deployment branches tomain.@taskless/cli→ Settings → Trusted Publisher → GitHub Actions, with:tasklessskillsrelease.ymlnpm-production(must match the workflow)Rollout sequence
Merge this → the
versionjob auto-opens a Version Packages PR (bumps@taskless/cli0.9.0 → 0.10.0, consumes the 3 pending changesets, runssync-skill-versions) → do the npm setup above → merge the Version PR → thepublishjob publishes 0.10.0 to npm via OIDC.Verify before un-drafting
npm publishfrompackages/cli(npm's OIDC is the most mature); flag if you'd rather route throughpnpm/changeset publish.Refs the release step needed to close TSKL-245 / TSKL-270 (shipping 0.10.0).