-
Notifications
You must be signed in to change notification settings - Fork 1
326 lines (304 loc) · 17.7 KB
/
Copy pathmain.yaml
File metadata and controls
326 lines (304 loc) · 17.7 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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
name: Build
# IMPORTANT: npm Trusted Publisher bindings for the 21 published @adobe/* packages point
# to this file (adobe/spacecat-shared / .github/workflows/main.yaml). Renaming or moving
# this file breaks OIDC publishing — re-run scripts/setup-npm-trusted-publishers.sh after.
# Renaming the 'npm-publish' GitHub Environment referenced below has the same silent-break
# failure mode — re-run the setup script after.
#
# Trust binding security relies on the 'npm-publish' GitHub Environment having a
# deployment_branch_policy that allows only `main`. That branch policy is the server-side
# enforcement of the OIDC token's `environment` claim — without it, the binding's
# `--environment npm-publish` filter is decorative. Branch protection on `main` (PR review
# required, disallow direct push) is the second leg of the security model.
#
# Required reviewers on the 'npm-publish' environment are INTENTIONALLY disabled to
# avoid a single-timezone bottleneck on routine releases (semantic-release auto-publishes
# every meaningful merge to `main`). To re-enable a human gate, see "Re-enabling the
# environment approval gate" in docs/RELEASE-RUNBOOK.md. Branch protection on `main`
# remains the load-bearing PR review gate; the env approval is a release-stamp on
# something already approved at PR time.
# See: https://docs.npmjs.com/trusted-publishers
# No workflow-level permissions — each job declares only what it needs.
permissions: {}
on:
push:
# workflow_dispatch enables manual re-trigger for incident recovery
# (see docs/RELEASE-RUNBOOK.md "Failure mode 3"). The release job's
# `if: github.ref == 'refs/heads/main'` and `environment: npm-publish`
# (deployment_branch_policy: main-only, no required reviewers — see header)
# still gate publish.
workflow_dispatch:
env:
CI_BUILD_NUM: ${{ github.run_id }}
CI_BRANCH: ${{ github.ref_name }}
jobs:
test:
name: Test
runs-on: ubuntu-latest
permissions:
id-token: write # AWS ECR OIDC (data-access IT tests)
contents: read
issues: read
steps:
- name: Check out
uses: actions/checkout@v6
with:
persist-credentials: 'false'
- name: Verify SR_NO_NPM_AUTH guard consistency
# @semantic-release/npm must be skipped in the Test-job dry-run: that job has no
# npm publish auth (OIDC works only in the release job, which has id-token: write
# + the npm-publish environment), so the plugin's verifyConditions npm-whoami would
# fail here. The skip is driven by SR_NO_NPM_AUTH: 'true' on the dry-run step below,
# gated in every .releaserc.cjs. This is PERMANENT, not a bootstrap shim — #1592
# added SR_NO_NPM_AUTH in the same change that removed NPM_TOKEN from the dry-run.
# This step enforces that the env var AND the guard in every .releaserc.cjs stay
# present; neither may be removed (doing so red-lines the dry-run on every PR).
#
# The anchored regex (indented key + 'true' | "true" | true) is robust to quote-style
# reformatting from Prettier/yamllint and is syntactically distinct from the JS
# strict-equality this step looks for in configs, so it cannot self-reference.
run: |
set -euo pipefail
shopt -s nullglob
files=(.releaserc.cjs packages/*/.releaserc.cjs)
if [ ${#files[@]} -eq 0 ]; then
echo "FAIL: no .releaserc.cjs files found — guard check cannot verify anything."
echo " Has the monorepo layout changed? Update this step's file glob."
exit 1
fi
# Per-file existence assertion: catches a stale glob expansion or a
# symlink-to-nowhere that would otherwise turn into grep exit code 2
# (silently swallowed by `|| true` below).
for f in "${files[@]}"; do
[ -f "$f" ] || { echo "FAIL: expected config not found or not a regular file: $f"; exit 1; }
done
# The SR_NO_NPM_AUTH skip is permanent (see comment above). Enforce BOTH legs:
# the workflow env var must stay declared, and every .releaserc.cjs must keep the
# matching guard. Removing either breaks the dry-run.
#
# Anchor: YAML mapping `<indent>SR_NO_NPM_AUTH: 'true' | "true" | true` at
# end-of-value (no trailing chars that would denote a different key/value).
if ! grep -qE "^[[:space:]]+SR_NO_NPM_AUTH:[[:space:]]+('true'|\"true\"|true)[[:space:]]*\$" .github/workflows/main.yaml; then
echo "FAIL: SR_NO_NPM_AUTH: 'true' is missing from the dry-run step env."
echo ""
echo "It is required and permanent — the Test-job dry-run has no npm publish auth,"
echo "so @semantic-release/npm must be skipped there. Restore it; do not remove it."
exit 1
fi
# Every .releaserc.cjs must include the strict guard. Accept both JS quote styles
# so Prettier reformats don't cause spurious fails.
missing=$(grep -L -E "SR_NO_NPM_AUTH === ['\"]true['\"]" "${files[@]}" || true)
if [ -n "$missing" ]; then
echo "FAIL: SR_NO_NPM_AUTH guard missing in:"
echo "$missing"
echo ""
echo "Each .releaserc.cjs must gate @semantic-release/npm on:"
echo " ...(process.env.SR_NO_NPM_AUTH === 'true' ? [] : [\"@semantic-release/npm\"]),"
exit 1
fi
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Node & NPM
uses: ./.github/actions/setup-node-npm
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
env:
MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN: ${{ secrets.MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN }}
- name: Verify package-lock.json is in sync
# Lockfile drift guard. semantic-release bumps each package's
# package.json + CHANGELOG on release but does NOT regenerate the root
# package-lock.json (the @semantic-release/git `assets` list in every
# .releaserc.cjs is package.json + CHANGELOG.md only). When a released
# workspace version later crosses a consumer's exact internal pin (e.g.
# cloud-manager-client -> spacecat-shared-ims-client), the lock loses
# the matching resolution and EVERY branch then fails `npm ci` with
# "Missing <pkg>@<ver> from lock file" — a main-wide breakage that first
# surfaces on unrelated PRs. Regenerate the lock with the same pinned
# npm (11.13.0, installed by the Setup Node & NPM composite above) and
# fail if it changed, so drift is caught on the PR that introduces it.
# The Release job keeps main's lock in sync after every release (see the
# "Sync package-lock.json after release" step), so on a healthy main
# this regeneration is a no-op.
shell: bash
run: |
set -euo pipefail
npm install --package-lock-only --ignore-scripts
if ! git diff --quiet -- package-lock.json; then
echo "FAIL: package-lock.json is out of sync with the workspace package.json files."
echo " Fix: run 'npm install --package-lock-only' locally and commit package-lock.json."
echo "----- drift -----"
git --no-pager diff -- package-lock.json
exit 1
fi
echo "OK: package-lock.json is in sync."
- name: Lint, Test, Coverage Upload
uses: ./.github/actions/lint-test-coverage
with:
ecr_pull_role_arn: ${{ secrets.AWS_ECR_PULL_ROLE_ARN }}
aws_region: us-east-1
- name: Semantic Release (Dry Run)
if: github.ref != 'refs/heads/main'
run: npm run semantic-release-dry
env:
GITHUB_TOKEN: ${{ secrets.ADOBE_BOT_GITHUB_TOKEN }}
# SR_NO_NPM_AUTH skips @semantic-release/npm in the plugin chain (see all .releaserc.cjs).
# PERMANENT, not a bootstrap shim: this dry-run has no npm publish auth (OIDC is
# release-job-only), so @semantic-release/npm's verifyConditions npm-whoami would fail
# here without it. #1592 added it in the same change that removed NPM_TOKEN from this
# step. Do not remove (the "Verify SR_NO_NPM_AUTH guard consistency" step enforces it).
SR_NO_NPM_AUTH: 'true'
release:
name: Release
runs-on: ubuntu-latest
needs: test
# Skip the release job on the chore(release) commits semantic-release pushes
# back to main. The guard matches semantic-release-monorepo's actual commit
# subject prefix (`chore(release):`) rather than the substring `[skip ci]`
# anywhere in the message — a substring match misfires on any commit whose
# body or PR description quotes the skip-ci convention (which trapped this
# PR's own commit 395f4921; see commit 2475850e for the workaround).
# Belt-and-suspenders: also AND with [skip ci] presence so a future
# chore(release): subject without [skip ci] does not get silently skipped.
if: github.ref == 'refs/heads/main' && !(startsWith(github.event.head_commit.message, 'chore(release)') && contains(github.event.head_commit.message, '[skip ci]'))
environment: npm-publish
timeout-minutes: 15
concurrency:
# Serialize releases on main so semantic-release's tag-then-publish flow
# (which assumes serialized execution) cannot race when two PRs land close
# together. cancel-in-progress: false — a half-completed release leaves
# inconsistent state on npm + git tags; let an in-flight one finish.
group: npm-publish-main
cancel-in-progress: false
permissions:
id-token: write # npm OIDC Trusted Publishing
contents: write
issues: read
env:
MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN: ${{ secrets.MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN }}
# NPM_CONFIG_PROVENANCE makes sigstore (status.sigstore.dev) a release prerequisite.
# Sigstore outage break-glass: temporarily set to 'false' to publish without
# provenance attestation for that release, then revert once sigstore recovers.
NPM_CONFIG_PROVENANCE: 'true'
steps:
- name: Check out
uses: actions/checkout@v6
with:
persist-credentials: 'false'
- name: Set up Node.js
uses: actions/setup-node@v6
with:
# Single source of truth — keeps the release job's Node version in
# lockstep with the test job's (which pulls from .nvmrc via the
# setup-node-npm composite action). A future .nvmrc bump no longer
# has to be applied in two places.
node-version-file: '.nvmrc'
- name: Update NPM
run: npm install -g npm@11.13.0
- name: Configure Git auth for private Mysticat types repo
run: |
git config --global url."https://x-access-token:${MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN}@github.com/adobe/mysticat-data-service".insteadOf "https://github.com/adobe/mysticat-data-service"
git config --global --add url."https://x-access-token:${MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN}@github.com/adobe/mysticat-data-service".insteadOf "ssh://git@github.com/adobe/mysticat-data-service"
git config --global --add url."https://x-access-token:${MYSTICAT_DATA_SERVICE_REPO_READ_TOKEN}@github.com/adobe/mysticat-data-service".insteadOf "git@github.com:adobe/mysticat-data-service"
- name: Install dependencies
run: npm ci
- name: Semantic Release
run: npm run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.ADOBE_BOT_GITHUB_TOKEN }}
- name: Sync package-lock.json after release
# semantic-release bumps the released package versions but does not
# regenerate the root package-lock.json (see the Test job's drift
# guard). Left unsynced, the lock's resolutions for internal exact pins
# go stale the moment a released workspace version crosses a consumer's
# pin, breaking `npm ci` on every branch. Regenerate the lock against
# the just-published versions and, if it changed, commit it straight to
# main with [skip ci] (GitHub natively skips workflow runs for commits
# whose head message contains [skip ci], so this triggers no new CI or
# release run). Best-effort: this must NEVER fail a release — the
# packages are already published and the Test-job drift guard is the
# backstop — hence continue-on-error. If the push loses a race it is
# left for the next PR's drift guard to surface.
if: success()
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ secrets.ADOBE_BOT_GITHUB_TOKEN }}
run: |
set -euo pipefail
# continue-on-error keeps this step from failing a release, which also
# means a failure is otherwise invisible. Surface ANY failure (regen,
# commit, or the push+rebase retry) as a workflow ::warning:: + run
# summary line so the operator sees it without digging through logs;
# the next PR's drift guard is still the backstop (RELEASE-RUNBOOK
# Failure mode 7). errexit makes a failed `npm install` an honest red
# mark instead of a false "already in sync" — the `if ! git push`
# retry below is errexit-safe (the negated condition suppresses it).
trap 'echo "::warning title=package-lock sync after release failed::Lockfile sync did not complete; see RELEASE-RUNBOOK Failure mode 7. The next PR drift guard will surface residual drift."; echo "package-lock.json sync after release failed — see RELEASE-RUNBOOK Failure mode 7." >> "$GITHUB_STEP_SUMMARY"' ERR
npm install --package-lock-only --ignore-scripts
if git diff --quiet -- package-lock.json; then
echo "OK: package-lock.json already in sync; nothing to commit."
exit 0
fi
echo "package-lock.json drifted after release; committing a sync commit to main."
git config user.name "semantic-release-bot"
git config user.email "semantic-release-bot@martynus.net"
git add package-lock.json
# `chore(release):` subject so the release-job skip guard above also
# skips this commit (belt-and-suspenders alongside the native [skip ci]
# skip), matching how that guard is documented.
git commit -m "chore(release): sync package-lock.json after release [skip ci]"
# Authenticate via http.extraHeader (base64 Basic auth) rather than
# embedding the token in the remote URL. git echoes the remote URL in
# push/pull error output, so a URL-embedded token can leak into logs if
# Actions' secret masking misses the exact substring; the header value
# is never echoed. Mirrors how actions/checkout injects credentials.
# Pre-mask the base64 form (which is NOT auto-masked, being a distinct
# string from the raw secret) before it can reach any log line.
auth_b64="$(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 | tr -d '\n')"
echo "::add-mask::${auth_b64}"
remote="https://github.com/${GITHUB_REPOSITORY}.git"
git_auth() { git -c "http.extraheader=AUTHORIZATION: basic ${auth_b64}" "$@"; }
# Releases are serialized (concurrency: npm-publish-main), so a race is
# unlikely, but retry once against the latest main just in case. KNOWN
# LIMITATION: a SECOND concurrent push landing within this retry window
# still loses — by design we do not loop. continue-on-error keeps it
# from failing the release, the ERR trap surfaces a ::warning::, and the
# residual drift is recovered via the next PR's drift guard +
# RELEASE-RUNBOOK Failure mode 7 (the manual lockfile-only PR).
if ! git_auth push "${remote}" HEAD:main; then
echo "push rejected; rebasing on latest main and retrying once."
git_auth pull --rebase "${remote}" main
git_auth push "${remote}" HEAD:main
fi
echo "Pushed package-lock.json sync to main."
- name: Surface failure pointer to release runbook
# On failure OR in-progress cancellation, write a pointer to the runbook
# into $GITHUB_STEP_SUMMARY so the on-call sees the recovery procedures
# in the PR check UI rather than having to remember where the runbook
# lives. cancelled() covers a mid-flight cancel (e.g. operator cancels
# during `npm publish`) — it does NOT fire for an FM-6 env-approval
# stall, since a job cancelled while still "Waiting for approval" never
# runs any step. The runbook itself does not depend on this pointer for
# FM-6 recovery.
if: failure() || cancelled()
shell: bash
env:
# Absolute URL — relative `../blob/main/...` from an Actions run page
# would resolve to /<owner>/<repo>/actions/blob/main/... (404).
RUNBOOK_URL: ${{ github.server_url }}/${{ github.repository }}/blob/main/docs/RELEASE-RUNBOOK.md
run: |
cat >> "$GITHUB_STEP_SUMMARY" <<EOF
## Release job did not complete successfully
See [\`docs/RELEASE-RUNBOOK.md\`](${RUNBOOK_URL}):
- **FM-1** — First OIDC release fails (revert path)
- **FM-2** — Sigstore unavailable (break-glass: \`NPM_CONFIG_PROVENANCE: 'false'\`)
- **FM-3** — Partial publish (15-min timeout / mid-flow failure)
- **FM-4** — Workflow / environment renamed
- **FM-5** — Setup-script preflight refused to register bindings
- **FM-6** — Env-approval stall
Trust binding: \`{repo: ${{ github.repository }}, workflow: main.yaml, environment: npm-publish}\`.
Tracking: SITES-42702.
EOF