-
Notifications
You must be signed in to change notification settings - Fork 0
177 lines (163 loc) · 9.27 KB
/
Copy pathrelease.yml
File metadata and controls
177 lines (163 loc) · 9.27 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
name: release
# Publishes every @ultimat3 package to npm using OIDC trusted publishing.
# npm mints a short-lived token from the GitHub Actions id-token and verifies it against the
# trusted publisher configured for each package on npmjs.com. Provenance is automatic.
#
# Prerequisites (one-time, per package, on npmjs.com — see PUBLISHING.md):
# 1. The package must already exist on the registry (bootstrap the first version by hand).
# 2. Package → Settings → Trusted Publisher → GitHub Actions:
# Organization/user: developerz-ai
# Repository: ultimate
# Workflow filename: release.yml
# Environment: npm-publish
#
# Two things must be TRUE outside this file for the gate below to be a gate, and both are named in
# PUBLISHING.md as human steps: the `npm-publish` environment needs required reviewers configured in
# Settings → Environments, and every package in the derived list needs a trusted publisher plus its
# first manual publish. Trusted publishing cannot bootstrap a package that does not exist yet, so a
# package added AFTER a release run is unpublished until a human bootstraps it — and this run will
# reach it and abort with everything ahead of it already published irreversibly.
# `bun run scripts/registry-audit.ts` answers whether that is owed, before a release finds out.
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to publish, e.g. 1.3.0. Must match every package.json in the tree.'
required: true
type: string
# Serialize releases, but NEVER cancel one mid-flight — npm publishes are irreversible, so
# cancel-in-progress must stay false (repo CI standard).
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
id-token: write # REQUIRED for OIDC — lets npm mint a token from this run
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 20
# The gate on the one irreversible job in the repo. This job holds `id-token: write`, which IS
# the repository's npm publishing identity — so before it, anyone who could dispatch a workflow
# could publish to @ultimat3 from an arbitrary branch, with provenance attesting that it came
# from here. A deployment environment is the only GitHub control that can hold a job with that
# permission until a human approves it; the two protections it carries (required reviewers, and
# a deployment tag rule of `v*`) are repository settings, not YAML, and are listed in
# PUBLISHING.md. The ref check below is the half that takes effect with no settings at all.
environment:
name: npm-publish
url: https://www.npmjs.com/org/ultimat3
steps:
# First, before checkout: `workflow_dispatch` accepts any ref the Actions UI will list, and
# `github.ref` is the only thing that says which one this is. A release published for a tag
# runs at `refs/tags/<tag>`, which is the ONLY shape a publish may have — the version below is
# read from that tag, and a branch has no version to read.
- name: refuse to publish from anything that is not a release tag
env:
REF: ${{ github.ref }}
run: |
set -euo pipefail
case "$REF" in
refs/tags/v*) echo "ok: publishing from $REF" ;;
*)
echo "::error::refusing to publish from $REF — an npm publish cannot be undone, so it runs from a vX.Y.Z tag only. Tag the release commit (git tag v1.3.0 && git push --follow-tags), then dispatch this workflow with that tag selected, or publish a GitHub Release for it."
exit 1
;;
esac
- uses: actions/checkout@v7
with:
persist-credentials: false
# No `registry-url:` on purpose. It writes an .npmrc containing
# //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
# and with no NODE_AUTH_TOKEN in the environment that resolves to an empty
# credential, which npm prefers over the OIDC exchange — publish then fails
# ENEEDAUTH on a workflow that is otherwise configured correctly. Trusted
# publishing needs no .npmrc at all.
# Node must be >= 22.14.0 for trusted publishing.
- uses: actions/setup-node@v7
with:
node-version: 22.14.0
# Trusted publishing needs npm >= 11.5.1; Node 22 ships an older npm.
# Pinned (not @latest): npm 11.6.x regressed the provenance path — a global install lands
# without a resolvable `sigstore` module, so `npm publish` with provenance dies on
# "Cannot find module 'sigstore'". 11.5.2 is the last release before that regression and
# satisfies the trusted-publishing minimum. Revisit when a fixed latest ships.
- name: upgrade npm
run: npm install -g npm@11.5.2
# SHA-pinned and series-pinned for the reasons in .github/actions/setup/action.yml, and this
# is the job the rule exists for: it holds `id-token: write`.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: '1.4.0'
- name: install
run: bun install --frozen-lockfile
# ONE source for the version: the tag this run is checked out at. Both events supply it —
# a published release runs at `refs/tags/<tag_name>`, and a dispatch runs at whichever ref was
# selected, which step 1 has already narrowed to `refs/tags/v*`. The `version` input is a
# confirmation, not a second source: a dispatcher who selects `v1.2.0` and types `1.3.0` has
# two different releases in mind, and the run stops rather than picking one.
- name: resolve the version being published
id: release
env:
REF: ${{ github.ref }}
INPUT: ${{ inputs.version }}
run: |
set -euo pipefail
version="${REF#refs/tags/v}"
if [ -n "${INPUT:-}" ] && [ "${INPUT#v}" != "$version" ]; then
echo "::error::this run is checked out at $REF but the version input says ${INPUT} — select the tag you mean, or type the version that tag carries"
exit 1
fi
echo "publishing $version"
echo "version=$version" >> "$GITHUB_OUTPUT"
# Before the 10-minute gate, because it costs a second and catches the failure the gate
# cannot: `verify`'s lockstep rule compares the 29 packages to EACH OTHER, so 29 manifests all
# reading 1.2.0 pass it while the tag says v1.10.1 — and every publish then dies
# EPUBLISHCONFLICT on a version already on the registry. `As of 2026-08` this tree was in that
# state: `git describe` answered v1.10.1-37-g837adfa with every package.json at 1.2.0, so a
# release published for that tag would have done exactly that. `scripts/release.ts` claimed
# this workflow already ran it; it did not.
- name: the repo is stamped at the version this tag claims
run: bun run scripts/release.ts --check "${{ steps.release.outputs.version }}" --json
# The gate runs before anything reaches the registry: an npm publish cannot be undone.
- name: verify
run: bun run scripts/verify.ts --json
# Published in dependency-tier order — a package must be on the registry before anything that
# imports it — and the list is DERIVED, never kept by hand.
#
# It used to be seven hand-written steps of `-w` flags, and `@ultimat3/flags` was on none of
# them, so the registry answered 404 for it while its siblings published: flags declares the
# same `publishConfig`, every consumer resolves it through the workspace, and nothing in this
# repo could notice. The missing entry was the symptom; a list that has to match a derived one
# is the defect, and adding one line would have fixed that day and re-broken on the next
# package somebody added — which is exactly what happened to `@ultimat3/scraping`, added after
# the 2.0.0 run and 404 until it was bootstrapped by hand on 2026-08-19.
#
# `scripts/list-workspaces.ts` reads the real package.json files rather than the tier table,
# so a package that exists on disk cannot be invisible here — and `create-ultimate` lands last
# because it is tier 6, not because a step says so.
- name: publish, tier by tier
run: |
set -euo pipefail
plan="$(bun run scripts/list-workspaces.ts --json | jq -r '
.data
| map(select(.publish == "public"))
| group_by(.tier) | sort_by(.[0].tier)
| .[] | "\(.[0].tier)\t\(map(.name) | join(" "))"')"
if [ -z "$plan" ]; then
echo "::error::no publishable workspaces — scripts/list-workspaces.ts returned an empty set, which means this checkout is not the repo it should be"
exit 1
fi
while IFS="$(printf '\t')" read -r tier names; do
[ -n "$tier" ] || continue
args=""
for name in $names; do args="$args -w $name"; done
echo "::group::npm publish — tier $tier: $names"
# Unquoted on purpose: `$args` is a list of flags this step just built, and quoting it
# would hand npm one argument containing spaces.
# shellcheck disable=SC2086
npm publish $args
echo "::endgroup::"
done <<< "$plan"