Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# SPDX-License-Identifier: MIT
# Automated changesets release. Adapted from the pattern in
# thecodedrift/firebot-script-music-to-my-ears and hardened for npm publishing.
#
# Security model (why it is split into two jobs):
#
# `version` reads contributor-authored changesets (UNTRUSTED text) and opens
# the "Version Packages" PR. It has NO npm credential and NO OIDC
# identity, so a crafted changeset / PR body has nothing to steal
# or escape into. The changeset TEXT is fully consumed here (into
# the CHANGELOG + PR body) and never reaches the credentialed job.
#
# `publish` runs only when main's version is not yet on npm — i.e. right
# after the Version Packages PR merges. By then there are no
# changesets left, so this job sees no untrusted PR/changeset text;
# it builds from reviewed, merged source only. It authenticates to
# npm with a SHORT-LIVED token minted via GitHub OIDC (npm trusted
# publishing) — there is NO stored NPM_TOKEN anywhere to exfiltrate.
#
# Residual perimeter, stated honestly: the publish job builds merged repo code,
# so "what can merge to main" is the real boundary. That is enforced by branch
# protection (review required) on main. `--ignore-scripts` keeps dependency
# lifecycle hooks from running while the OIDC identity is available; only our
# own build runs. No `pull_request_target` and no `${{ }}` interpolation of
# untrusted text into any `run:` — the two classic token-exfiltration footguns.
#
# Action refs are pinned to commit SHAs (supply-chain hardening); the trailing
# comment records the human-readable tag.

name: Release

on:
push:
branches: [main]

# Serialize releases so two pushes can't race the version PR / publish.
concurrency: release-${{ github.ref }}

# No workflow-wide grants; each job requests exactly what it needs.
permissions: {}

jobs:
version:
name: Version Packages PR
runs-on: ubuntu-latest
permissions:
contents: write # push the changeset-release/main branch
pull-requests: write # open/update the Version Packages PR
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
Comment thread
Copilot marked this conversation as resolved.
- run: pnpm install --frozen-lockfile --ignore-scripts

# `version: pnpm bump` runs `changeset version` AND `sync-skill-versions`,
# so the bumped version is propagated into skills/recipes in the same PR.
# No `publish:` input — this job can never publish.
- uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1
with:
version: pnpm bump
commit: "chore: version packages"
title: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Decides whether main's version needs publishing — with NO credential and NO
# OIDC identity. Runs on every push, but the credentialed publish job below
# only starts when this reports a new version, so ordinary non-release pushes
# never instantiate an OIDC-capable job or touch the npm-production environment.
check:
name: Check for a new version
runs-on: ubuntu-latest
permissions:
contents: read # checkout + read package.json
outputs:
publish: ${{ steps.check.outputs.publish }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false # no git writes here; don't leave the token in git config
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
# Publish only a version npm has never seen. On an ordinary feature merge
# the version is unchanged (already on npm) so this is false; it flips true
# only on the merge of the Version Packages PR. Reads package.json (repo
# source) via `node -p` — no untrusted interpolation into the shell.
- id: check
run: |
name=$(node -p "require('./packages/cli/package.json').name")
version=$(node -p "require('./packages/cli/package.json').version")
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "$name@$version already published — nothing to do."
else
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "Will publish $name@$version."
fi

publish:
name: Publish to npm
# Gate on the credential-free check: this job — and therefore the OIDC
# identity + npm-production environment — only exists for an actual release.
needs: check
if: needs.check.outputs.publish == 'true'
runs-on: ubuntu-latest
# Environment is the scoping/audit boundary for the release and where the
# npm trusted-publisher is bound. No required reviewers (fully automatic
# once the Version Packages PR merges), by design.
environment: npm-production
permissions:
contents: read # checkout only
id-token: write # OIDC → short-lived npm auth + build provenance
Comment thread
thecodedrift marked this conversation as resolved.
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false # publish authenticates via OIDC/npm, not git creds
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
registry-url: https://registry.npmjs.org
Comment thread
Copilot marked this conversation as resolved.
- run: pnpm install --frozen-lockfile --ignore-scripts

# OIDC trusted publishing + provenance need npm >= 11.5.1. Pin the version
# (not @latest) so the release is deterministic and a new npm release
# can't change publish behavior unreviewed; bump this intentionally.
# --ignore-scripts: no lifecycle code runs while the OIDC identity exists.
- run: npm install -g npm@12.0.1 --ignore-scripts

- run: pnpm --filter @taskless/cli build

# OIDC handshake happens here (id-token: write + registry-url + a
# registered trusted publisher). No token in env. `--provenance` attaches
# a signed build-provenance attestation.
- run: npm publish --provenance --access public
working-directory: packages/cli
Loading