Repository navigation
279 lines (260 loc) · 13.2 KB
/
Copy pathrelease-plz.yml
File metadata and controls
279 lines (260 loc) · 13.2 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
name: "Release crates (crates.io)"
# Publishes two crates.io release lines via release-plz (crates.io Trusted
# Publishing over OIDC — no CARGO_REGISTRY_TOKEN): `eql-bindings` from
# packages/eql (`release`), and the stack-* crates from the root workspace
# (`release-crates`, described above its own jobs at the end). `release` runs
# after `release-crates`: see the comment on its `needs:`.
# Everything from here to `permissions:` is about the EQL line.
#
# THE FILENAME CANNOT CHANGE: crates.io binds the publisher to it.
#
# Publish-only. Changesets owns the version — the Version Packages PR bumps
# crates/eql-bindings/Cargo.toml in lockstep with the npm package via
# scripts/sync-lockstep-versions.mjs. A release-plz `release-pr` job would
# fight it, so there isn't one. The tag is `eql-bindings-v<semver>`, chosen so a
# crate release does not fire the `eql-*` SQL/docs/image automation.
#
# INERT until the Phase-5 cutover: the `release` job is gated on
# scripts/eql-pipeline-armed.mjs, which reads FROZEN_PUBLISHERS. Before the
# first crate release this repository also needs a GPG_PRIVATE_KEY secret (it
# has none) and a crates.io Trusted Publishing entry naming cipherstash/stack +
# release-plz.yml. Neither is needed for a run that publishes nothing: the
# preflight step decides that before the key is imported.
# Read-only by default; the publishing job escalates for itself. Enforced by
# scripts/__tests__/workflow-publish-permissions.test.mjs.
permissions:
contents: read
on:
push:
branches:
- main
# Scoped to the subtree. Upstream had no filter and did not need one: it was
# an EQL-ONLY repository, where "every push to main" and "every EQL change"
# were the same set. Here they are not, and this job holds crates.io OIDC,
# `contents: write` and the signing key — booting it on every merge to a
# monorepo is a standing credential exposure for no release.
#
# A release that moves the crate always matches: `changeset version` runs
# scripts/sync-lockstep-versions.mjs, which rewrites
# packages/eql/crates/eql-bindings/Cargo.toml and the SQL assets beside it.
# Kept in step with the other EQL filters by
# scripts/__tests__/eql-workflow-filters.test.mjs.
# The stack-* crates line adds the two published crates and the root
# workspace and release-plz inputs.
paths:
- "packages/eql/**"
- ".github/workflows/release-plz.yml"
- "packages/stack-auth/**"
- "packages/stack-profile/**"
- "Cargo.toml"
- "Cargo.lock"
- "release-plz.toml"
- "cliff.toml"
workflow_dispatch: {}
# Never cancel — a cancelled release can leave a half-published state.
concurrency:
group: release-plz
cancel-in-progress: false
jobs:
# No registry lookup, so it cannot race release.yml's npm publish on the same
# push. See scripts/eql-pipeline-armed.mjs.
eql-armed:
name: Is the EQL release line armed?
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
armed: ${{ steps.armed.outputs.armed }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
package-manager-cache: false
# No pnpm install: the script imports node builtins only.
- name: Read the publisher switch
id: armed
run: node scripts/eql-pipeline-armed.mjs
release:
name: "Release"
# AFTER `release-crates`, not beside it. eql-bindings' `stack-encrypt`
# feature names a stack-encrypt VERSION, and `cargo publish` resolves it
# from crates.io, so on a push that releases both, publishing in parallel
# races this job's resolution against the other job's upload. Ordered,
# the stack-* crates land first and this job's verify finds them.
# `!cancelled()`, not the implicit `success()`: `release-crates` is
# skipped on every push that moves no stack-* crate, and a skipped
# dependency would otherwise skip this job too. Not `always()` either:
# that would start this job after somebody cancelled the run, and a
# crates.io upload cannot be undone, only yanked (see the same rule on
# the publishing jobs in release.yml). A FAILURE of `release-crates`
# still stops this one, since the crates it needed did not publish.
needs: [eql-armed, release-crates]
if: >-
!cancelled()
&& needs.eql-armed.outputs.armed == 'true'
&& (needs.release-crates.result == 'success' || needs.release-crates.result == 'skipped')
# GitHub-hosted, matching this repo's other publishing jobs.
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write # the eql-bindings-v<V> tag and release
pull-requests: write # release-plz links PRs in release notes
id-token: write # crates.io Trusted Publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
# No mise step (upstream had one): nothing here runs a mise task, and
# release-plz needs only cargo and git, which the runner image ships.
- name: Verify bundled SQL matches the crate version
# The crate `include_str!`s COMMITTED SQL and release-plz publishes the
# committed tree verbatim, so a hand-pinned Cargo.toml would ship the
# DEV placeholder as its "exact SQL". Only enforced when this run would
# actually publish; a crates.io API failure fails towards enforcing.
#
# ALSO decides whether the GPG import below runs. This step used to sit
# BELOW the key import, which meant a repository with no GPG_PRIVATE_KEY
# failed here on every push to main the moment the pipeline was armed —
# including pushes that release nothing. Nothing published (the import
# fails closed), but main went permanently red with an error that reads
# like a broken release rather than a missing secret. Ordering the
# registry question first is what makes a no-op run a no-op.
id: preflight
working-directory: packages/eql
run: |
set -euo pipefail
cargo_version="$(grep -m1 '^version = ' crates/eql-bindings/Cargo.toml | cut -d'"' -f2)"
# crates.io 403s curl's default User-Agent, which would make the
# short-circuit below unreachable.
published="$(curl -fsSL --retry 3 -H "User-Agent: cipherstash-eql-release (https://github.com/cipherstash/stack)" "https://crates.io/api/v1/crates/eql-bindings/versions" | jq -r '.versions[].num' | grep -Fx "$cargo_version" || true)"
if [[ -n "$published" ]]; then
echo "eql-bindings@${cargo_version} is already on crates.io; publish no-op — skipping the bundled-SQL guard"
echo "publish_needed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Empty `published` also covers a crates.io API failure, so this is
# the fail-towards-enforcing branch: guard the SQL, import the key.
echo "publish_needed=true" >> "$GITHUB_OUTPUT"
manifest_version="$(jq -r '.eqlVersion' crates/eql-bindings/sql/release-manifest.json)"
if [[ "$manifest_version" != "$cargo_version" ]]; then
echo "::error::release-manifest.json eqlVersion ('$manifest_version') does not match Cargo.toml ('$cargo_version'). Run 'pnpm run version' from the repository root." >&2
exit 1
fi
# THE STAMP, not the schema name. Upstream grepped for `eql_v3`, which
# appears 23,000 times in every build including a DEV one, so the
# guard could not fire. tasks/build.sh writes the version into this
# COMMENT and it is the only record of which build produced the bytes
# — the manifest is regenerated alongside them and goes on agreeing.
if ! grep -q "COMMENT ON SCHEMA eql_v3 IS '${cargo_version}'" crates/eql-bindings/sql/cipherstash-encrypt.sql; then
stamped="$(grep -o "COMMENT ON SCHEMA eql_v3 IS '[^']*'" crates/eql-bindings/sql/cipherstash-encrypt.sql | head -1)"
echo "::error::bundled SQL was not built for ${cargo_version} — it carries ${stamped:-no version stamp}. Run 'pnpm run version' from the repository root." >&2
exit 1
fi
- name: Import GPG key
# release-plz signs the commit and tag it creates. When the preflight
# says the version is already published it creates neither, so there is
# nothing to sign and no reason to require the secret.
if: steps.preflight.outputs.publish_needed == 'true'
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
git_tag_gpgsign: true
- name: Run release-plz release
uses: release-plz/action@8e61445f0b34e7c6d985255f2f1f05a36eb350dc # v0.5
with:
command: release
# Required here and absent upstream: a composite action's steps do not
# inherit the caller's working-directory, so without these release-plz
# reads the monorepo root, finds no `eql-bindings` workspace, and
# exits 0 having published nothing.
manifest_path: packages/eql/Cargo.toml
config: packages/eql/release-plz.toml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ---- The stack-* crates line (stack-auth, stack-profile) -----------------
#
# Its own job, not a step in `release`: a step there would inherit the EQL
# gate, and the two lines arm at different times.
#
# ARMED by the arming PR of the stack-* crates import. No crate is in
# FROZEN_PUBLISHERS (an npm map), so the switch keys on the
# `@cipherstash/auth` entry, which that PR deleted when crates.io and npm
# trusted publishing moved here together. crates.io needs a Trusted
# Publishing entry for every published crate naming cipherstash/stack +
# release-plz.yml.
#
# Publish-only: nothing here opens a release PR, so a version bump of these
# crates is a hand-made pull request. `release-plz update` cannot version
# this workspace yet: it walks back to the commit that matches crates.io
# 0.42.3, and the imported history has no root Cargo.toml there. A
# `release-pr` job comes after the first release from this repository.
crates-armed:
name: Is the stack-* crates release line armed?
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
armed: ${{ steps.armed.outputs.armed }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
package-manager-cache: false
- name: Read the publisher switch
id: armed
run: node scripts/eql-pipeline-armed.mjs crates
release-crates:
name: "Release the stack-* crates"
needs: [crates-armed]
if: needs.crates-armed.outputs.armed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write # the <crate>-v<V> tags and releases
pull-requests: write # release-plz links PRs in release notes
id-token: write # crates.io Trusted Publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Is any crate version unpublished?
# The EQL preflight's crates.io question, per published crate. It decides whether the GPG import runs; an API failure reads
# as "unpublished", which fails towards importing the key.
id: preflight
run: |
set -euo pipefail
publish_needed=false
for crate in stack-auth stack-profile stack-kms stack-encrypt stack-encrypt-derive; do
version="$(grep -m1 '^version = ' "packages/${crate}/Cargo.toml" | cut -d'"' -f2)"
# crates.io 403s curl's default User-Agent.
published="$(curl -fsSL --retry 3 -H "User-Agent: cipherstash-stack-release (https://github.com/cipherstash/stack)" "https://crates.io/api/v1/crates/${crate}/versions" | jq -r '.versions[].num' | grep -Fx "$version" || true)"
if [[ -n "$published" ]]; then
echo "${crate}@${version} is already on crates.io"
else
echo "${crate}@${version} is not on crates.io; it will be published"
publish_needed=true
fi
done
echo "publish_needed=${publish_needed}" >> "$GITHUB_OUTPUT"
- name: Import GPG key
if: steps.preflight.outputs.publish_needed == 'true'
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
git_tag_gpgsign: true
- name: Run release-plz release (root workspace)
uses: release-plz/action@8e61445f0b34e7c6d985255f2f1f05a36eb350dc # v0.5
with:
command: release
manifest_path: Cargo.toml
config: release-plz.toml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}