-
Notifications
You must be signed in to change notification settings - Fork 0
161 lines (153 loc) · 8.07 KB
/
Copy pathrelease-cli.yml
File metadata and controls
161 lines (153 loc) · 8.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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