Skip to content

ci: hardened changesets release workflow (OIDC, no stored npm token)#57

Merged
thecodedrift merged 4 commits into
mainfrom
jakob/release-workflow
Jul 10, 2026
Merged

ci: hardened changesets release workflow (OIDC, no stored npm token)#57
thecodedrift merged 4 commits into
mainfrom
jakob/release-workflow

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Jul 10, 2026

Copy link
Copy Markdown
Member

Automates changeset releases so the "Version Packages" PR is rebuilt as work lands on main, and @taskless/cli publishes to npm when that PR merges — without a long-lived npm token. Adapted from the pattern in thecodedrift/firebot-script-music-to-my-ears and hardened for npm publishing. MIT licensed work

Security 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 when main'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 stored NPM_TOKEN anywhere.

Also: least-privilege per job, --ignore-scripts + explicit build, --provenance, action SHAs pinned, push: main only (never pull_request_target), and no ${{ }} interpolation of untrusted text into any run:.

Residual perimeter, stated honestly: the publish job builds merged code, so what can merge to main is 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)

  1. Create the GitHub environment npm-production (repo Settings → Environments → New environment). No secrets needed; optionally restrict its deployment branches to main.
  2. Register the npm trusted publisher on npmjs.com → @taskless/cli → Settings → Trusted Publisher → GitHub Actions, with:
    • Organization/owner: taskless
    • Repository: skills
    • Workflow filename: release.yml
    • Environment: npm-production (must match the workflow)
  3. Confirm the package allows provenance/OIDC publishes (default).

Rollout sequence

Merge this → the version job auto-opens a Version Packages PR (bumps @taskless/cli 0.9.0 → 0.10.0, consumes the 3 pending changesets, runs sync-skill-versions) → do the npm setup above → merge the Version PR → the publish job publishes 0.10.0 to npm via OIDC.

Verify before un-drafting

  • OIDC trusted publishing needs npm ≥ 11.5.1; the workflow upgrades npm in the publish job — confirm that satisfies it in-runner.
  • We publish the single package with npm publish from packages/cli (npm's OIDC is the most mature); flag if you'd rather route through pnpm/changeset publish.

Refs the release step needed to close TSKL-245 / TSKL-270 (shipping 0.10.0).

@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Jul 10, 2026
@thecodedrift
thecodedrift requested a review from Copilot July 10, 2026 19:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml with a two-job model: uncredentialed version (creates/updates Version Packages PR) and credentialed publish (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.

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml Outdated
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
thecodedrift marked this pull request as ready for review July 10, 2026 19:17
thecodedrift and others added 2 commits July 10, 2026 13:01
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/release.yml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml Outdated
thecodedrift and others added 2 commits July 10, 2026 13:15
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
thecodedrift merged commit 075ea2f into main Jul 10, 2026
7 checks passed
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
thecodedrift deleted the jakob/release-workflow branch July 10, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants