-
Notifications
You must be signed in to change notification settings - Fork 0
307 lines (288 loc) · 15.5 KB
/
Copy pathrelease.yml
File metadata and controls
307 lines (288 loc) · 15.5 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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# Publish @codecavepro/brand to npm, from a pushed version tag.
#
# THE FILENAME IS PART OF THE CREDENTIAL. This job authenticates by OIDC
# ("trusted publishing"): no npm token is stored anywhere, and npmjs.com decides
# whether to mint a short-lived one by matching the org, the repository and the
# workflow FILENAME against what is registered on the package. Renaming this
# file revokes the ability to publish, and npm reports that as a 404 on the PUT
# rather than as an authentication failure — see "If the publish is refused"
# below. Register it as `release.yml`, with no directory part.
#
# What triggers a publish: pushing an annotated tag that is a bare version —
# `2.1.5`, no `v`, this repo's convention since 1.0.0. That is the whole
# ceremony, and it is deliberately a human act with a name on it.
#
# The order this inverts: RELEASING.md used to publish first and tag afterwards,
# because a human ran both. Now the tag is the trigger, so it comes first. What
# used to be a rehearsal a human could skip is the eight steps below, and they
# all run before anything reaches the registry — so a bad tag costs a red run,
# not a burned version number.
#
# What this job does NOT check, deliberately: whether the component sources
# still match codecave.pro. The site installs this package and pins it, so it
# is *supposed* to be behind between releases — see the note where that step
# used to be.
name: Publish @codecavepro/brand to npm
on:
push:
tags:
# Bare semver. The in-job guard below is the real gate — it asserts the
# tag and the manifest agree — so this pattern only has to keep unrelated
# tags from starting a run.
- '[0-9]+.[0-9]+.[0-9]+'
# For re-running a publish that failed on something outside the package: a
# registry blip, a trusted-publisher field that was wrong the first time.
# Select the TAG in the "Use workflow from" box, not a branch — the job
# refuses a branch ref, so that every published version has a tag.
workflow_dispatch:
permissions:
contents: read
# This is the credential. Without it npm has no OIDC token to exchange and
# the publish fails as though the package did not exist.
id-token: write
# Serialised, and never cancelled: a publish that is interrupted between the
# registry write and the confirmation below leaves nobody able to say whether
# the version went out.
concurrency:
group: npm-publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
# v7 removed setup-node's dummy NODE_AUTH_TOKEN export, which is what
# makes registry-url safe to use with OIDC: on earlier majors the .npmrc
# it writes carries an auth line that resolves to an empty token and
# shadows the OIDC exchange. Node 24 because trusted publishing needs
# node >= 22.14 and npm >= 11.5.1.
#
# Caching is off on purpose. npm's own guidance is never to cache in a
# release build; a restored node_modules is the one input to a published
# tarball that no commit accounts for.
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false
# Both floor versions are OIDC's, not this package's. A runner image that
# drifted below either would fail at the publish step with an
# authentication error that reads like a misconfigured trusted publisher,
# which is a long way to walk for a wrong npm.
#
# The pin is 24 and the floor is 22.14, so there is a wide margin here --
# which is the point of asserting rather than assuming: the margin is a
# property of the RUNNER IMAGE, not of this file, and setup-node resolves
# `24` to whatever 24.x the image currently offers. This was briefly
# pinned to 22, where the margin is one minor line instead of two majors
# and npm is the tighter half (floor 11.5.1; Node 22.23 bundles 11.7.0).
# Either way the remedy is in the failure message.
- name: Assert the toolchain can do OIDC
run: |
set -euo pipefail
node - <<'EOF'
const { execSync } = require('node:child_process');
/* -1 / 0 / 1, comparing dotted numeric versions field by field. A
missing field counts as 0, so "24" is above "22.14". */
const cmp = (a, b) => {
const x = String(a).split('.').map(Number);
const y = String(b).split('.').map(Number);
for (let i = 0; i < Math.max(x.length, y.length); i += 1) {
const d = (x[i] ?? 0) - (y[i] ?? 0);
if (d) return d < 0 ? -1 : 1;
}
return 0;
};
const npmv = execSync('npm --version').toString().trim();
const bad = [];
if (cmp(process.versions.node, '22.14') < 0) bad.push(`node ${process.versions.node} < 22.14`);
if (cmp(npmv, '11.5.1') < 0) bad.push(`npm ${npmv} < 11.5.1`);
if (bad.length) {
console.error('This runner cannot use trusted publishing: ' + bad.join(', '));
console.error('Raise node-version above, or pin npm with `npm i -g npm@latest`.');
process.exit(1);
}
console.log(`node ${process.versions.node}, npm ${npmv} — both above the OIDC floor.`);
EOF
# THE TAG IS THE VERSION. packages/brand/package.json is committed as
# 0.0.0 and stamped from this value further down, so there is no second
# place to bump and nothing to forget. This step only reads the ref; it
# writes nothing, because the manifest must not be touched before
# `npm ci` has checked it against the lockfile.
#
# What this replaces: an assertion that the tag equalled the manifest,
# which a human kept in step with a hand-run `npm version`. The first
# release nobody ran it for was refused by its own guard — tag 2.3.0,
# manifest 2.2.0 — which is the safe failure but still a failure, and the
# guard could never have been anything else: it compared the tag against
# a number that nothing moved.
- name: Read the version from the tag
id: version
run: |
set -euo pipefail
if [ "${{ github.ref_type }}" != "tag" ]; then
echo "::error title=Not a tag::This job publishes from a version tag. Re-run it with the tag selected in \"Use workflow from\", so that every published version has a tag."
exit 1
fi
VERSION="${{ github.ref_name }}"
# Belt and braces on the `tags:` filter above: workflow_dispatch can
# select any ref, and that route does not go through it.
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error title=Not a version tag::\"$VERSION\" is not a bare version. Tags in this repo are the version and nothing else — 2.3.0, not v2.3.0."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing $VERSION, named by the tag."
# Asked before the build rather than after, because npm's own refusal
# arrives three minutes later and the answer never changes in between.
# `npm view` exits non-zero for a version that does not exist, which is
# the case we want, so the failure is the success here.
- name: Assert this version is not already published
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -uo pipefail
if npm view "@codecavepro/brand@$VERSION" version >/dev/null 2>&1; then
echo "::error title=Already published::@codecavepro/brand@$VERSION is on the registry. A version number can never be reissued — bump and tag again."
exit 1
fi
LATEST="$(npm view @codecavepro/brand version 2>/dev/null || echo 'none')"
echo "Registry is at $LATEST; publishing $VERSION."
- name: Install
run: npm ci
# After `npm ci`, because a manifest stamped before it would disagree
# with package-lock.json and npm ci refuses that. Before the build,
# because `npm pack` names the tarball from this file and the smoke step
# installs that name.
#
# Not a `prepack` hook, which is where this belongs by shape and is wrong
# by behaviour: npm resolves the tarball's NAME before prepack runs and
# packs the manifest as prepack left it, so a prepack stamp yields
# codecavepro-brand-0.0.0.tgz whose package.json says 2.3.0. Which of
# those two the registry would believe is not worth finding out — a
# version number cannot be reissued. tools/stamp-version.mjs has the
# measurement.
- name: Stamp the version into the package manifest
run: node tools/stamp-version.mjs --require-tag
# Creates BOTH of the things `npm run check` reads and a fresh checkout
# does not have: packages/brand/dist/, and the compiled storybook bundles
# that check:importmap resolves against the vendored runtime map. Neither
# is committed — the storybook output stopped being tracked on
# 2026-08-27 — so running check straight after a checkout exits 1 on
# "no compiled specimens", which is a missing build and reads like a
# broken package.
#
# `build:storybook` rather than `build:package`: it chains the package
# build itself, because it compiles each specimen out of
# packages/brand/dist/src. The package build also runs the README value
# assertions, so a palette change that left the package page behind still
# stops here.
- name: Build the package and the storybook
run: npm run build:storybook
# The same suite CI already runs on every push to development, plus the
# package's own byte-identity check against docs/.
- name: Check
run: npm run check
# There is deliberately NO step here comparing the component sources to
# codecave.pro. This job briefly had one, and it was a rule from the
# arrangement that CCWEB2-318 replaced.
#
# The site consumes this package now and pins it with
# `pnpm install --frozen-lockfile`, so it runs whatever its lockfile says
# until someone raises the range. LAGGING IS ITS NORMAL STATE. A release
# is the moment the library moves furthest ahead of it — components are
# developed here, tested in the storybook here, published, and only then
# does the site bump. A gate demanding the two be equal would therefore be
# red for exactly the change it was meant to protect, and green only in
# the window when there is nothing to release.
#
# ---- rehearse on the actual artifact ---------------------------------
# Everything above reads the tree. This installs the tarball and asks it
# the questions a consumer asks first, because `files`, `exports` and
# npm's by-name pickup of LICENSE and README are all invisible to a check
# that walks dist/ in place. --legacy-peer-deps skips auto-installing the
# five peers: nothing here imports them, and vue is a large download to
# prove nothing with.
- name: Pack and smoke-test the tarball
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
SMOKE="$RUNNER_TEMP/smoke"
mkdir -p "$SMOKE"
npm pack --workspace @codecavepro/brand --pack-destination "$SMOKE"
(
cd "$SMOKE"
npm init -y >/dev/null
npm i "./codecavepro-brand-$VERSION.tgz" --no-audit --no-fund --legacy-peer-deps
)
node tools/smoke-tarball.mjs "$SMOKE/node_modules/@codecavepro/brand"
# ---- publish ---------------------------------------------------------
# `npm run release:package`, not a bare `npm publish`: the root manifest and the
# package manifest share the name `@codecavepro/brand`, so the two are
# distinguished by PATH and only by path. The script carries the
# --workspace flag that picks the right one. See RELEASING.md.
#
# No NODE_AUTH_TOKEN, no --otp, no secret. If this step fails, read "If
# the publish is refused" at the bottom of this file before changing
# anything.
- name: Publish
run: npm run release:package
# The publish exiting 0 is npm's account of the publish. This is the
# registry's.
- name: Confirm the registry agrees
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
for i in 1 2 3 4 5; do
PUBLISHED="$(npm view "@codecavepro/brand@$VERSION" version 2>/dev/null || true)"
[ -n "$PUBLISHED" ] && break
echo "Not visible yet; retrying ($i/5)."
sleep 6
done
if [ "${PUBLISHED:-}" != "$VERSION" ]; then
echo "::error title=Published version not visible::npm publish exited 0 but the registry does not serve $VERSION. Check https://www.npmjs.com/package/@codecavepro/brand before republishing anything — a version number can never be reissued."
exit 1
fi
{
echo "### Published \`@codecavepro/brand@$VERSION\`"
echo ""
echo "- <https://www.npmjs.com/package/@codecavepro/brand/v/$VERSION>"
echo "- Provenance is attached automatically — this ran as a trusted publisher."
echo ""
echo "**Still to do by hand:** comment the version and tag on"
echo "[CCWEB2-318](https://codecave.atlassian.net/browse/CCWEB2-318). If this"
echo "release changed a token VALUE, say which — codecave.pro consumes these, and"
echo "a value change is the only kind of release with downstream work attached."
echo ""
echo "codecave.pro will not move on its own: it installs with"
echo "\`pnpm install --frozen-lockfile\`, so it runs whatever its lockfile pins"
echo "until someone raises the range and regenerates it."
} >> "$GITHUB_STEP_SUMMARY"
echo "Registry serves $PUBLISHED."
# If the publish is refused
# -------------------------
# npm reports a failed OIDC exchange as `E404 Not Found - PUT
# https://registry.npmjs.org/@codecavepro%2fbrand`, which reads as "no such
# package" and is not that — @codecavepro/brand exists. Check, in this order:
#
# 1. The trusted publisher is registered on the PACKAGE, at
# npmjs.com/package/@codecavepro/brand/access, not on the org.
# 2. Workflow filename is `release.yml` — the bare filename, NOT
# `.github/workflows/release.yml`. Every field is case-sensitive and exact.
# 3. Organization is `CodeCavePro` and repository is `brand`.
# 4. Allowed actions includes `npm publish`.
# 5. Environment name is EMPTY. This job declares no environment; a value
# there will never match.
#
# One thing that is genuinely unproven: whether npm's OIDC exchange resolves the
# package correctly for `npm publish --workspace`, given that this package sits
# in a subdirectory and declares `repository.directory`. Trusted publishing has
# known rough edges with nested packages. If everything above is right and the
# PUT is still refused, the next thing to try is publishing from the package
# directory instead — `working-directory: packages/brand` with a bare `npm
# publish` — which reaches the same manifest by a plainer route. Do that as a
# workflow edit, so the two paths never disagree about which manifest is meant.