-
Notifications
You must be signed in to change notification settings - Fork 0
refactor(ci): split release.yml so each workflow carries one design #119
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
62c7bd6
refactor(ci): split release.yml so each workflow carries one design
thecodedrift a1d8503
refactor(ci): name the Vale workflow for what its file says
thecodedrift b3cc5ff
docs(changeset): say which operational details the split changes
thecodedrift 137b943
ci: guard the CLI publish the way the Vale publish is guarded
thecodedrift File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| "@taskless/cli": patch | ||
| --- | ||
|
|
||
| Split the release pipeline so each workflow file carries one release design. | ||
|
|
||
| `release.yml` held two jobs with opposite trust properties behind one header. | ||
| It is now `release-cli-changeset.yml` — which reads contributor-authored | ||
| changesets and opens the Version Packages PR holding no npm credential and no | ||
| OIDC identity — and `release-cli.yml`, which keeps the credential-free | ||
| "is this version already on npm?" gate together with the publish job it | ||
| protects, so an OIDC-capable job is never instantiated on an ordinary merge. | ||
| `vale-binaries.yml` is renamed `release-vale.yml` to match. | ||
|
|
||
| The build and publish steps themselves are unchanged — same triggers, same | ||
| `permissions: {}`, same action pins, same OIDC trusted publishing behind the | ||
| same `npm-production` approval. Two operational details do differ: `check` and | ||
| `publish` no longer share the `release-*` concurrency group, and the release | ||
| now runs as two workflow runs instead of one, so its check contexts are | ||
| `Release CLI Version PR / …` and `Release CLI / …` rather than `Release / …`. | ||
| Neither is a required check. | ||
|
|
||
| The header comments also get one correction: they claimed `npm-production` had | ||
| no required reviewers, and it has had one all along, so a release has always | ||
| waited for a human approval that the file said was not there. |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # SPDX-License-Identifier: MIT | ||
| # Version Packages PR for @taskless/cli. Adapted from the pattern in | ||
| # thecodedrift/firebot-script-music-to-my-ears. | ||
| # | ||
| # THIS WORKFLOW CANNOT PUBLISH, and that is its entire security property. | ||
| # | ||
| # It reads contributor-authored changesets (UNTRUSTED text) and folds them into | ||
| # a CHANGELOG and a pull request body. It holds NO npm credential and NO OIDC | ||
| # identity, so a crafted changeset or PR body has nothing here to steal and | ||
| # nothing to escape into. The changeset TEXT is fully consumed at this stage and | ||
| # never reaches a credentialed job: by the time `release-cli.yml` publishes, the | ||
| # Version Packages PR has merged and there are no changesets left to read. | ||
| # | ||
| # That is why the publish lives in its own file rather than a job below. Keeping | ||
| # untrusted text and an OIDC identity in one file invites a later edit that | ||
| # hands one to the other — e.g. an `outputs:` carrying changeset-derived text | ||
| # into a `run:` in a job holding `id-token: write`. | ||
| # | ||
| # There is no `publish:` input on the changesets action, deliberately. Supplying | ||
| # one would turn this job into a publisher while it is still holding untrusted | ||
| # input, which is the arrangement the split exists to prevent. | ||
| # | ||
| # CONCURRENCY is required here specifically. Two pushes racing on the | ||
| # `changeset-release/main` branch is a real failure: both would force the branch | ||
| # and one PR would end up describing versions the other computed. | ||
| # | ||
| # Action refs are pinned to commit SHAs (supply-chain hardening); the trailing | ||
| # comment records the human-readable tag. | ||
|
|
||
| name: Release CLI Version PR | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| # Serialize so two pushes can't race the Version Packages PR branch. | ||
| concurrency: release-${{ github.ref }} | ||
|
|
||
| # No workflow-wide grants; the 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 | ||
| - 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 }} |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| # SPDX-License-Identifier: MIT | ||
| # Publish @taskless/cli to npm. Adapted from the pattern in | ||
| # thecodedrift/firebot-script-music-to-my-ears and hardened for npm publishing. | ||
| # | ||
| # TWO JOBS IN ONE FILE, ON PURPOSE: | ||
| # | ||
| # check Decides whether main's version needs publishing — with NO npm | ||
| # credential and NO OIDC identity. It runs on every push to main and | ||
| # reads packages/cli/package.json (repo source) via `node -p`, so | ||
| # nothing untrusted is interpolated into a shell. | ||
| # | ||
| # publish Exists only when `check` says yes — i.e. right after the Version | ||
| # Packages PR merges, when the version in the manifest is one npm has | ||
| # never seen. It authenticates with a SHORT-LIVED token minted via | ||
| # GitHub OIDC (npm trusted publishing); there is NO stored NPM_TOKEN | ||
| # anywhere to exfiltrate. | ||
| # | ||
| # The gate and the job it gates MUST stay in the same file. The gate's real work | ||
| # is not saving a minute of CI — it is keeping an OIDC-capable job from being | ||
| # instantiated at all on an ordinary merge. Split across two files, a later edit | ||
| # that reads only the publish half would see a job with `id-token: write` and no | ||
| # visible reason for the `needs:`, and drop it. | ||
| # | ||
| # The untrusted half of the release lives in `release-cli-changeset.yml`, which | ||
| # holds no credential. By the time this workflow publishes, the changesets have | ||
| # already been consumed into the merged CHANGELOG, so this job builds from | ||
| # reviewed, merged source only. | ||
| # | ||
| # 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 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. | ||
| # | ||
| # NO CONCURRENCY GROUP HERE, deliberately — do not add one for tidiness. Two | ||
| # things stand in for serialization, and the second is the one that matters. | ||
| # The credential-free `check` gate makes the ordinary duplicate a no-op: the | ||
| # second run sees the version on npm and never instantiates this job. But | ||
| # `check` is a SEPARATE JOB, so between its answer and the publish there is a | ||
| # window where another run can ship the same version — serialization would | ||
| # close that window, and a gate in another job does not. | ||
| # | ||
| # What closes it is the `npm view` guard immediately before `npm publish`, | ||
| # below. Asking again at the moment it matters is idempotent rather than merely | ||
| # ordered: it also covers a re-run of this workflow against a version an | ||
| # earlier attempt already published, which serialization does nothing about. | ||
| # `release-vale.yml` guards each tarball the same way, for the same reason. | ||
| # | ||
| # Action refs are pinned to commit SHAs (supply-chain hardening); the trailing | ||
| # comment records the human-readable tag. | ||
|
|
||
| name: Release CLI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| # No workflow-wide grants; each job requests exactly what it needs. | ||
| permissions: {} | ||
|
|
||
| jobs: | ||
| # 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 — so ordinary 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 | ||
| - 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 for @taskless/cli is bound. npm-production carries a | ||
| # REQUIRED REVIEWER and a protected-branches deployment policy, so this job | ||
| # waits for a human to approve the deployment before it starts — the merge | ||
| # of the Version Packages PR is not by itself enough to ship. That click is | ||
| # deliberate and is the gate on what users get by default: a publish here | ||
| # moves the `latest` tag that every `npm i @taskless/cli` resolves. Flows | ||
| # that reach no user by default (the Vale platform packages, nightlies) use | ||
| # npm-autopublish instead and are gated by code review rather than a click. | ||
| environment: npm-production | ||
| permissions: | ||
| contents: read # checkout only | ||
| id-token: write # OIDC → short-lived npm auth + build provenance | ||
| 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 | ||
| - 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. | ||
| # | ||
| # The `npm view` guard immediately before the publish is what makes the | ||
| # missing concurrency group safe, and it has to live HERE rather than in | ||
| # the `check` job. `check` runs in a separate job, so between its answer | ||
| # and this line there is a window in which another run can publish the | ||
| # same version; serializing the workflow would close that window, but so | ||
| # does asking again at the moment it matters. Re-asking is the better of | ||
| # the two: it is idempotent rather than merely ordered, so it also covers | ||
| # a re-run of this workflow on a version an earlier attempt already | ||
| # published — which serialization does nothing about. | ||
| # | ||
| # This mirrors `release-vale.yml`, which guards each tarball the same way | ||
| # for the same reason. Without it, the losing run of a race fails with | ||
| # npm's "cannot publish over the previously published version", which | ||
| # reads as a broken release rather than as a duplicate that was already | ||
| # handled. | ||
| - name: Publish (skipping a version already on npm) | ||
| working-directory: packages/cli | ||
| run: | | ||
| name=$(node -p "require('./package.json').name") | ||
| version=$(node -p "require('./package.json').version") | ||
| if npm view "$name@$version" version >/dev/null 2>&1; then | ||
| echo "$name@$version is already published — nothing to do." | ||
| else | ||
| npm publish --provenance --access public | ||
| fi |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.