@cleocode/cleo 2026.5.77 → 2026.5.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ # CLEO release pipeline — Prepare bump-PR workflow.
2
+ #
3
+ # Generated from packages/cleo/templates/workflows/release-prepare.yml.tmpl by
4
+ # `cleo init --workflows` (T9531). DO NOT EDIT the rendered file directly —
5
+ # extend via `.workflow-overrides.yml` per SPEC-T9345 R-260.
6
+ #
7
+ # Implements SPEC-T9345-release-pipeline-v2.md §5.1 (R-200 — R-210, R-260 —
8
+ # R-263). Triggers ONLY on workflow_dispatch; runs preflight (lint +
9
+ # typecheck + test + build) BEFORE creating any commit; prepares the
10
+ # release/<version> branch and opens the bump-PR via `gh pr create`.
11
+ #
12
+ # Placeholders (replaced at scaffold time — see workflows/README.md):
13
+ # <NODE_VERSION> Node.js version (e.g. "22.x")
14
+ # <INSTALL_CMD> Install command (e.g. "pnpm install --frozen-lockfile")
15
+ # <LINT_CMD> tool:lint (ADR-061 resolved)
16
+ # <TYPECHECK_CMD> tool:typecheck (ADR-061 resolved)
17
+ # <TEST_CMD> tool:test (ADR-061 resolved)
18
+ # <BUILD_CMD> tool:build (ADR-061 resolved)
19
+ # <BRANCH_PREFIX> Release branch prefix (default: "release")
20
+ # <PR_LABEL> GitHub label applied to bump-PR (default: "release")
21
+ # (Angle-brackets in this doc comment are intentional — they avoid being
22
+ # substituted by the {{...}} regex pass. The placeholders below use {{...}}.)
23
+
24
+ name: Release Prepare
25
+
26
+ # R-200: workflow_dispatch only — MUST NOT trigger on push to any branch.
27
+ # R-201: required input `version`; optional `plan-blob-sha256`, `epic`, `channel`.
28
+ on:
29
+ workflow_dispatch:
30
+ inputs:
31
+ version:
32
+ description: 'Version to release (e.g. v2026.6.0)'
33
+ type: string
34
+ required: true
35
+ plan-blob-sha256:
36
+ description: 'SHA256 of pre-computed plan JSON (optional)'
37
+ type: string
38
+ required: false
39
+ epic:
40
+ description: 'Epic task ID (e.g. T9999) used by `cleo release plan`'
41
+ type: string
42
+ required: false
43
+ channel:
44
+ description: 'Release channel'
45
+ type: choice
46
+ required: false
47
+ default: latest
48
+ options:
49
+ - latest
50
+ - beta
51
+ - alpha
52
+ - rc
53
+
54
+ # R-202: contents:write (commit + branch), pull-requests:write (gh pr create),
55
+ # id-token:write (signed tags only). MUST NOT request packages:write — npm
56
+ # publish belongs to release-publish.yml.
57
+ permissions:
58
+ contents: write
59
+ pull-requests: write
60
+ id-token: write
61
+
62
+ # R-207: concurrent prepare runs against the same version queue rather than
63
+ # collide. cancel-in-progress=false preserves in-flight work.
64
+ concurrency:
65
+ group: release-prepare-${{ inputs.version }}
66
+ cancel-in-progress: false
67
+
68
+ # R-262: bash everywhere — no cross-platform shell drift.
69
+ defaults:
70
+ run:
71
+ shell: bash
72
+
73
+ jobs:
74
+ # R-204: lint + typecheck + test + build BEFORE any commit. Failure here
75
+ # MUST fail the workflow and prevent `prepare` from running.
76
+ preflight:
77
+ name: Preflight (lint + typecheck + test + build)
78
+ runs-on: ubuntu-latest
79
+ timeout-minutes: 20
80
+ steps:
81
+ # R-263: third-party Actions pinned to major+minor.
82
+ - name: Checkout
83
+ uses: actions/checkout@v4.1
84
+ with:
85
+ fetch-depth: 0
86
+ timeout-minutes: 5
87
+
88
+ - name: Set up Node.js
89
+ uses: actions/setup-node@v4.0
90
+ with:
91
+ node-version: '{{NODE_VERSION}}'
92
+ timeout-minutes: 3
93
+
94
+ - name: Install dependencies
95
+ run: {{INSTALL_CMD}}
96
+ timeout-minutes: 5
97
+
98
+ - name: Lint
99
+ run: {{LINT_CMD}}
100
+ timeout-minutes: 5
101
+
102
+ - name: Typecheck
103
+ run: {{TYPECHECK_CMD}}
104
+ timeout-minutes: 5
105
+
106
+ - name: Test
107
+ run: {{TEST_CMD}}
108
+ timeout-minutes: 10
109
+
110
+ - name: Build
111
+ run: {{BUILD_CMD}}
112
+ timeout-minutes: 10
113
+
114
+ # R-208: status check `release-prepare/preflight` is reported by GitHub
115
+ # automatically against the bump-PR head SHA once the PR is open. No
116
+ # extra step is required — branch protection MUST require this check.
117
+
118
+ # R-203: prepare needs preflight.
119
+ # R-205: plan resolution → version bump → CHANGELOG → commit → push → bump-PR.
120
+ prepare:
121
+ name: Prepare bump-PR
122
+ runs-on: ubuntu-latest
123
+ needs: preflight
124
+ timeout-minutes: 20
125
+ outputs:
126
+ branch: ${{ steps.branch.outputs.name }}
127
+ branch_created: ${{ steps.push.outputs.created }}
128
+ env:
129
+ # R-210: only GITHUB_TOKEN required. NPM_TOKEN is publish-only.
130
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
131
+ steps:
132
+ - name: Checkout
133
+ uses: actions/checkout@v4.1
134
+ with:
135
+ fetch-depth: 0
136
+ token: ${{ secrets.GITHUB_TOKEN }}
137
+ timeout-minutes: 5
138
+
139
+ - name: Configure git identity
140
+ run: |
141
+ git config user.email "actions@github.com"
142
+ git config user.name "GitHub Actions"
143
+ timeout-minutes: 1
144
+
145
+ - name: Cut release branch
146
+ id: branch
147
+ run: |
148
+ BRANCH="{{BRANCH_PREFIX}}/${{ inputs.version }}"
149
+ git checkout -b "$BRANCH"
150
+ echo "name=$BRANCH" >> "$GITHUB_OUTPUT"
151
+ timeout-minutes: 2
152
+
153
+ - name: Set up Node.js
154
+ uses: actions/setup-node@v4.0
155
+ with:
156
+ node-version: '{{NODE_VERSION}}'
157
+ timeout-minutes: 3
158
+
159
+ - name: Install dependencies
160
+ run: {{INSTALL_CMD}}
161
+ timeout-minutes: 5
162
+
163
+ # R-205(a)/(b): resolve plan — call `cleo release plan` if sha is empty,
164
+ # otherwise verify the existing plan blob via sha256.
165
+ - name: Resolve release plan
166
+ id: plan
167
+ run: |
168
+ mkdir -p .cleo/release
169
+ PLAN_FILE=".cleo/release/${{ inputs.version }}.plan.json"
170
+ if [ -z "${{ inputs.plan-blob-sha256 }}" ]; then
171
+ echo "No plan-blob-sha256 input — generating fresh plan."
172
+ EPIC_ARGS=()
173
+ if [ -n "${{ inputs.epic }}" ]; then
174
+ EPIC_ARGS=(--epic "${{ inputs.epic }}")
175
+ fi
176
+ cleo release plan "${{ inputs.version }}" "${EPIC_ARGS[@]}" --json > "$PLAN_FILE"
177
+ else
178
+ echo "Verifying pre-computed plan against sha256=${{ inputs.plan-blob-sha256 }}"
179
+ if [ ! -f "$PLAN_FILE" ]; then
180
+ echo "::error::Plan file $PLAN_FILE not found — cannot verify sha256"
181
+ exit 1
182
+ fi
183
+ ACTUAL=$(sha256sum "$PLAN_FILE" | awk '{print $1}')
184
+ if [ "$ACTUAL" != "${{ inputs.plan-blob-sha256 }}" ]; then
185
+ echo "::error::Plan sha256 mismatch — expected ${{ inputs.plan-blob-sha256 }}, got $ACTUAL"
186
+ exit 1
187
+ fi
188
+ fi
189
+ echo "plan_file=$PLAN_FILE" >> "$GITHUB_OUTPUT"
190
+ timeout-minutes: 5
191
+
192
+ # R-205(c): bump version files via `cleo version-bump`.
193
+ - name: Bump version
194
+ run: cleo version-bump --version "${{ inputs.version }}"
195
+ timeout-minutes: 3
196
+
197
+ # R-205(d): regenerate CHANGELOG via the preserved `cleo release
198
+ # changelog` primitive, scoped to commits since the previous tag.
199
+ - name: Regenerate CHANGELOG
200
+ run: |
201
+ PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
202
+ if [ -n "$PREV_TAG" ]; then
203
+ cleo release changelog --since "$PREV_TAG"
204
+ else
205
+ cleo release changelog
206
+ fi
207
+ timeout-minutes: 3
208
+
209
+ # R-205(e): commit with the canonical subject line.
210
+ - name: Commit prepare
211
+ run: |
212
+ git add -A
213
+ git commit -m "release: prepare ${{ inputs.version }}"
214
+ timeout-minutes: 2
215
+
216
+ - name: Push branch
217
+ id: push
218
+ run: |
219
+ git push origin "${{ steps.branch.outputs.name }}"
220
+ echo "created=true" >> "$GITHUB_OUTPUT"
221
+ timeout-minutes: 5
222
+
223
+ # R-205(f): open the bump-PR via `gh pr create`.
224
+ - name: Open bump-PR
225
+ run: |
226
+ gh pr create \
227
+ --base main \
228
+ --head "${{ steps.branch.outputs.name }}" \
229
+ --title "release: prepare ${{ inputs.version }}" \
230
+ --label "{{PR_LABEL}}" \
231
+ --body-file "${{ steps.plan.outputs.plan_file }}"
232
+ timeout-minutes: 5
233
+
234
+ # R-209(c): emit recovery hint into the job summary artifact.
235
+ - name: Emit recovery hint to job summary
236
+ if: always()
237
+ run: |
238
+ {
239
+ echo "## Release Prepare — ${{ inputs.version }}"
240
+ echo ""
241
+ echo "Branch: \`${{ steps.branch.outputs.name }}\`"
242
+ echo ""
243
+ echo "**Recovery command** (if anything went wrong):"
244
+ echo ""
245
+ echo "\`\`\`"
246
+ echo "cleo release plan ${{ inputs.version }} --epic ${{ inputs.epic }}; cleo release open ${{ inputs.version }}"
247
+ echo "\`\`\`"
248
+ } >> "$GITHUB_STEP_SUMMARY"
249
+ timeout-minutes: 1
250
+
251
+ # R-209(a)(b): on failure, delete the half-cut release branch and print
252
+ # the recovery command. continue-on-error so cleanup itself cannot fail
253
+ # the workflow further.
254
+ cleanup-on-failure:
255
+ name: Cleanup on failure
256
+ runs-on: ubuntu-latest
257
+ needs:
258
+ - preflight
259
+ - prepare
260
+ if: failure()
261
+ timeout-minutes: 10
262
+ env:
263
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
264
+ steps:
265
+ - name: Checkout
266
+ uses: actions/checkout@v4.1
267
+ with:
268
+ fetch-depth: 0
269
+ token: ${{ secrets.GITHUB_TOKEN }}
270
+ timeout-minutes: 5
271
+
272
+ - name: Delete release branch (best-effort)
273
+ continue-on-error: true
274
+ run: |
275
+ BRANCH="{{BRANCH_PREFIX}}/${{ inputs.version }}"
276
+ if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
277
+ echo "Deleting remote branch $BRANCH"
278
+ git push origin --delete "$BRANCH" || true
279
+ else
280
+ echo "No remote branch $BRANCH to delete."
281
+ fi
282
+ timeout-minutes: 5
283
+
284
+ - name: Print recovery command
285
+ run: |
286
+ echo "::notice::Recovery: cleo release plan ${{ inputs.version }} --epic ${{ inputs.epic }}; cleo release open ${{ inputs.version }}"
287
+ {
288
+ echo "## Release Prepare FAILED — ${{ inputs.version }}"
289
+ echo ""
290
+ echo "Run this to recover:"
291
+ echo ""
292
+ echo "\`\`\`"
293
+ echo "cleo release plan ${{ inputs.version }} --epic ${{ inputs.epic }}; cleo release open ${{ inputs.version }}"
294
+ echo "\`\`\`"
295
+ } >> "$GITHUB_STEP_SUMMARY"
296
+ timeout-minutes: 1
@@ -0,0 +1,388 @@
1
+ # CLEO release pipeline — Publish + tag workflow.
2
+ #
3
+ # Generated from packages/cleo/templates/workflows/release-publish.yml.tmpl by
4
+ # `cleo init --workflows` (T9531). DO NOT EDIT the rendered file directly —
5
+ # extend via `.workflow-overrides.yml` per SPEC-T9345 R-260.
6
+ #
7
+ # Implements SPEC-T9345-release-pipeline-v2.md §5.2 (R-220 — R-235, R-260 —
8
+ # R-263). Triggers on push to main when the commit subject matches the
9
+ # canonical `release: prepare v<version>` prefix produced by
10
+ # release-prepare.yml. Tags ONLY after `gh pr view` confirms state=MERGED
11
+ # AND mergeCommit.oid == $GITHUB_SHA — this eliminates the F6 race
12
+ # (tag-on-pre-merge-SHA) by construction.
13
+ #
14
+ # Placeholders (replaced at scaffold time — see workflows/README.md):
15
+ # <NODE_VERSION> Node.js version (e.g. "22.x")
16
+ # <INSTALL_CMD> Install command (e.g. "pnpm install --frozen-lockfile")
17
+ # <LINT_CMD> tool:lint (ADR-061 resolved)
18
+ # <TYPECHECK_CMD> tool:typecheck (ADR-061 resolved)
19
+ # <TEST_CMD> tool:test (ADR-061 resolved)
20
+ # <BUILD_CMD> tool:build (ADR-061 resolved)
21
+ # <NPM_PUBLISH_CMD> Publish command (e.g. "pnpm publish --access public --tag latest")
22
+ # <PUBLISHERS> Space-separated publisher list (e.g. "npm cargo")
23
+ # (Angle-brackets in this doc comment are intentional — they avoid being
24
+ # substituted by the {{...}} regex pass. The placeholders below use {{...}}.)
25
+
26
+ name: Release Publish
27
+
28
+ # R-220: push to main filtered to version-file paths only — bypasses unrelated
29
+ # main commits without spinning up the full matrix.
30
+ # R-221: workflow_dispatch with `version` input for manual re-runs and
31
+ # idempotency tests.
32
+ on:
33
+ push:
34
+ branches:
35
+ - main
36
+ paths:
37
+ - 'package.json'
38
+ - 'packages/*/package.json'
39
+ - 'Cargo.toml'
40
+ - 'packages/*/Cargo.toml'
41
+ - 'pyproject.toml'
42
+ - 'packages/*/pyproject.toml'
43
+ workflow_dispatch:
44
+ inputs:
45
+ version:
46
+ description: 'Version to publish (e.g. v2026.6.0)'
47
+ type: string
48
+ required: true
49
+
50
+ # R-222: workflow-level grants the minimum read+tag scopes. `packages: write`
51
+ # is granted PER-JOB on publish-and-tag only — fanout/reconcile jobs MUST
52
+ # NOT inherit publish scope.
53
+ permissions:
54
+ contents: write
55
+ id-token: write
56
+
57
+ # R-230: concurrent publishes against the same version queue rather than
58
+ # collide. cancel-in-progress=false preserves in-flight publishes.
59
+ concurrency:
60
+ group: release-publish-${{ github.sha }}
61
+ cancel-in-progress: false
62
+
63
+ # R-262: bash everywhere — no cross-platform shell drift across the matrix.
64
+ defaults:
65
+ run:
66
+ shell: bash
67
+
68
+ jobs:
69
+ # R-223 (1/4) + R-224: classify the triggering commit. If the subject
70
+ # doesn't match `release: prepare v<version>`, ALL downstream jobs skip
71
+ # (`should_publish=false`). Manual workflow_dispatch always publishes.
72
+ detect:
73
+ name: Detect release commit
74
+ runs-on: ubuntu-latest
75
+ timeout-minutes: 3
76
+ outputs:
77
+ should_publish: ${{ steps.classify.outputs.should_publish }}
78
+ version: ${{ steps.classify.outputs.version }}
79
+ steps:
80
+ # R-263: third-party Actions pinned to major+minor.
81
+ - name: Checkout
82
+ uses: actions/checkout@v4.1
83
+ with:
84
+ fetch-depth: 0
85
+ timeout-minutes: 5
86
+
87
+ - name: Classify triggering commit
88
+ id: classify
89
+ run: |
90
+ set -euo pipefail
91
+ if [ -n "${{ inputs.version }}" ]; then
92
+ # Manual dispatch — trust the input.
93
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
94
+ echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
95
+ exit 0
96
+ fi
97
+ BEFORE="${{ github.event.before }}"
98
+ SHA="${{ github.sha }}"
99
+ if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
100
+ BEFORE="$SHA^"
101
+ fi
102
+ if subject=$(git log --format=%s "$BEFORE..$SHA" 2>/dev/null | grep -E '^release: prepare v' | head -1); then
103
+ VERSION=$(echo "$subject" | sed -E 's/^release: prepare (v[^ ]+).*/\1/')
104
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
105
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
106
+ else
107
+ echo "should_publish=false" >> "$GITHUB_OUTPUT"
108
+ echo "version=" >> "$GITHUB_OUTPUT"
109
+ fi
110
+ timeout-minutes: 2
111
+
112
+ # R-223 (2/4) + R-225: matrix expansion across the five T1737 platform
113
+ # tuples. fail-fast=true so a single platform failure prevents the
114
+ # publish-and-tag job from running (R-228).
115
+ build-matrix:
116
+ name: Build & test (${{ matrix.platform }})
117
+ needs: detect
118
+ if: needs.detect.outputs.should_publish == 'true'
119
+ strategy:
120
+ fail-fast: true
121
+ matrix:
122
+ include:
123
+ - platform: linux-x64
124
+ runner: ubuntu-latest
125
+ - platform: linux-arm64
126
+ runner: ubuntu-22.04-arm
127
+ - platform: macos-x64
128
+ runner: macos-15-intel
129
+ - platform: macos-arm64
130
+ runner: macos-14
131
+ - platform: windows-x64
132
+ runner: windows-latest
133
+ runs-on: ${{ matrix.runner }}
134
+ timeout-minutes: 30
135
+ steps:
136
+ - name: Checkout
137
+ uses: actions/checkout@v4.1
138
+ with:
139
+ fetch-depth: 0
140
+ timeout-minutes: 5
141
+
142
+ - name: Set up Node.js
143
+ uses: actions/setup-node@v4.0
144
+ with:
145
+ node-version: '{{NODE_VERSION}}'
146
+ timeout-minutes: 3
147
+
148
+ - name: Install dependencies
149
+ run: {{INSTALL_CMD}}
150
+ timeout-minutes: 10
151
+
152
+ - name: Lint
153
+ run: {{LINT_CMD}}
154
+ timeout-minutes: 5
155
+
156
+ - name: Typecheck
157
+ run: {{TYPECHECK_CMD}}
158
+ timeout-minutes: 5
159
+
160
+ - name: Test
161
+ run: {{TEST_CMD}}
162
+ timeout-minutes: 15
163
+
164
+ - name: Build
165
+ run: {{BUILD_CMD}}
166
+ timeout-minutes: 10
167
+
168
+ # R-225: integration smoke — `cleo --version` and `cleo briefing --json
169
+ # | jq .ok`. ANTHROPIC_API_KEY is scoped to the environment so an
170
+ # unmerged PR can't smoke-test against production credentials.
171
+ - name: Integration smoke
172
+ run: |
173
+ set -euo pipefail
174
+ ./bin/cleo --version
175
+ ./bin/cleo briefing --json | jq '.ok'
176
+ env:
177
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
178
+ timeout-minutes: 5
179
+
180
+ # R-232: on failure the matrix slot artifacts are uploaded for
181
+ # 30-day retention so operators can triage cross-platform regressions.
182
+ - name: Upload build artifact
183
+ if: always()
184
+ uses: actions/upload-artifact@v4.3
185
+ with:
186
+ name: cleo-${{ needs.detect.outputs.version }}-${{ matrix.platform }}
187
+ path: |
188
+ dist/
189
+ packages/*/dist/
190
+ if-no-files-found: warn
191
+ retention-days: 30
192
+ timeout-minutes: 5
193
+
194
+ # R-223 (3/4) + R-226: publish + tag. Gated by `environment: cleo-publish`
195
+ # (Letta §3.4 step 7) for reviewer-policy enforcement. R-228 enforces
196
+ # that any matrix slot failure blocks this job via the `needs` edge.
197
+ publish-and-tag:
198
+ name: Publish & tag
199
+ needs:
200
+ - detect
201
+ - build-matrix
202
+ if: needs.detect.outputs.should_publish == 'true'
203
+ runs-on: ubuntu-latest
204
+ environment: cleo-publish
205
+ timeout-minutes: 25
206
+ # R-222: packages:write scoped to THIS job only.
207
+ permissions:
208
+ contents: write
209
+ packages: write
210
+ id-token: write
211
+ env:
212
+ VERSION: ${{ needs.detect.outputs.version }}
213
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
214
+ # `{{PUBLISHERS}}` is a render-time placeholder. Hoisting it into env
215
+ # turns the downstream `contains(env.PUBLISHERS, 'npm')` into a
216
+ # dynamic expression so actionlint doesn't flag it as a constant `if:`.
217
+ PUBLISHERS: '{{PUBLISHERS}}'
218
+ steps:
219
+ - name: Checkout
220
+ uses: actions/checkout@v4.1
221
+ with:
222
+ fetch-depth: 0
223
+ token: ${{ secrets.GITHUB_TOKEN }}
224
+ timeout-minutes: 5
225
+
226
+ - name: Configure git identity
227
+ run: |
228
+ git config user.email "actions@github.com"
229
+ git config user.name "GitHub Actions"
230
+ timeout-minutes: 1
231
+
232
+ # R-229 (CRITICAL — F6 race elimination): poll `gh pr view` and assert
233
+ # state=MERGED AND mergeCommit.oid == $GITHUB_SHA BEFORE issuing any
234
+ # `git tag`. Mismatch fails with E_TAG_MISMATCH and never pushes.
235
+ - name: Confirm PR merge state (R-229)
236
+ id: confirm
237
+ run: |
238
+ set -euo pipefail
239
+ PR_NUMBER=$(gh pr list \
240
+ --search "${{ github.sha }}" \
241
+ --state merged \
242
+ --json number \
243
+ --jq '.[0].number // empty')
244
+ if [ -z "$PR_NUMBER" ]; then
245
+ echo "::error::E_TAG_MISMATCH no merged PR found for SHA ${{ github.sha }}"
246
+ exit 1
247
+ fi
248
+ PR_JSON=$(gh pr view "$PR_NUMBER" --json state,mergeCommit)
249
+ STATE=$(echo "$PR_JSON" | jq -r '.state')
250
+ MERGE_OID=$(echo "$PR_JSON" | jq -r '.mergeCommit.oid // ""')
251
+ if [ "$STATE" != "MERGED" ]; then
252
+ echo "::error::E_TAG_MISMATCH PR #$PR_NUMBER state=$STATE (expected MERGED)"
253
+ exit 1
254
+ fi
255
+ if [ -z "$MERGE_OID" ] || [ "$MERGE_OID" != "${{ github.sha }}" ]; then
256
+ echo "::error::E_TAG_MISMATCH PR #$PR_NUMBER merge_oid=$MERGE_OID expected=${{ github.sha }}"
257
+ exit 1
258
+ fi
259
+ echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
260
+ echo "merge_oid=$MERGE_OID" >> "$GITHUB_OUTPUT"
261
+ timeout-minutes: 5
262
+
263
+ - name: Set up Node.js
264
+ uses: actions/setup-node@v4.0
265
+ with:
266
+ node-version: '{{NODE_VERSION}}'
267
+ registry-url: 'https://registry.npmjs.org'
268
+ timeout-minutes: 3
269
+
270
+ - name: Install dependencies
271
+ run: {{INSTALL_CMD}}
272
+ timeout-minutes: 10
273
+
274
+ - name: Build
275
+ run: {{BUILD_CMD}}
276
+ timeout-minutes: 10
277
+
278
+ # R-226(b): npm publish — gated on PUBLISHERS env containing "npm".
279
+ - name: Publish npm packages
280
+ if: contains(env.PUBLISHERS, 'npm')
281
+ run: {{NPM_PUBLISH_CMD}}
282
+ env:
283
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
284
+ timeout-minutes: 10
285
+
286
+ # R-226(c): cargo publish — gated on PUBLISHERS env containing "cargo".
287
+ - name: Publish cargo crates
288
+ if: contains(env.PUBLISHERS, 'cargo')
289
+ run: cargo publish --token "${{ secrets.CARGO_TOKEN }}"
290
+ timeout-minutes: 10
291
+
292
+ # R-226(d): tag the merge commit (NOT $GITHUB_SHA directly — they're
293
+ # equivalent here but using the confirm step's output makes the
294
+ # F6-eliminating proof explicit).
295
+ - name: Tag release
296
+ run: |
297
+ set -euo pipefail
298
+ git tag "$VERSION" "${{ steps.confirm.outputs.merge_oid }}"
299
+ git push origin "$VERSION"
300
+ timeout-minutes: 5
301
+
302
+ # R-226(e): GitHub Release with auto-generated notes, matrix
303
+ # artifacts attached. fail_on_unmatched_files=true asserts artifacts
304
+ # actually exist.
305
+ - name: Download matrix artifacts
306
+ uses: actions/download-artifact@v4.1
307
+ with:
308
+ path: release-artifacts/
309
+ merge-multiple: false
310
+ timeout-minutes: 5
311
+
312
+ - name: Create GitHub Release
313
+ uses: softprops/action-gh-release@v2.0
314
+ with:
315
+ tag_name: ${{ env.VERSION }}
316
+ target_commitish: ${{ steps.confirm.outputs.merge_oid }}
317
+ generate_release_notes: true
318
+ fail_on_unmatched_files: true
319
+ files: |
320
+ release-artifacts/**/*
321
+ env:
322
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
323
+
324
+ # R-233: partial-publish failure handler — emits a critical issue when
325
+ # one registry succeeded but a later step failed.
326
+ - name: Emit partial-publish incident on failure
327
+ if: failure()
328
+ continue-on-error: true
329
+ run: |
330
+ gh issue create \
331
+ --label release-incident \
332
+ --title "Partial publish failure: ${VERSION}" \
333
+ --body "release-publish.yml failed AFTER one or more registries had succeeded. Manual reconciliation required: re-run reconcile or run \`cleo release reconcile ${VERSION}\` from a local checkout. Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
334
+ env:
335
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
336
+ timeout-minutes: 5
337
+
338
+ # R-223 (4/4) + R-227: provenance reconciliation. NON-BLOCKING —
339
+ # continue-on-error: true at the job level so reconcile failure cannot
340
+ # roll back the tag/publish. Failure opens a backfill issue.
341
+ reconcile:
342
+ name: Reconcile provenance
343
+ needs:
344
+ - detect
345
+ - publish-and-tag
346
+ if: needs.detect.outputs.should_publish == 'true'
347
+ runs-on: ubuntu-latest
348
+ timeout-minutes: 10
349
+ continue-on-error: true
350
+ env:
351
+ VERSION: ${{ needs.detect.outputs.version }}
352
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
353
+ steps:
354
+ - name: Checkout
355
+ uses: actions/checkout@v4.1
356
+ with:
357
+ fetch-depth: 0
358
+ timeout-minutes: 5
359
+
360
+ - name: Set up Node.js
361
+ uses: actions/setup-node@v4.0
362
+ with:
363
+ node-version: '{{NODE_VERSION}}'
364
+ registry-url: 'https://registry.npmjs.org'
365
+ timeout-minutes: 3
366
+
367
+ - name: Install cleo (freshly published version)
368
+ run: npm install -g "@cleocode/cleo@${VERSION#v}"
369
+ timeout-minutes: 5
370
+
371
+ - name: Reconcile release provenance
372
+ id: rec
373
+ continue-on-error: true
374
+ run: cleo release reconcile "$VERSION" --from-workflow --json
375
+ timeout-minutes: 5
376
+
377
+ # R-227: reconcile failure opens a backfill issue but MUST NOT roll
378
+ # back the tag or the publish.
379
+ - name: Open provenance-backfill issue on failure
380
+ if: steps.rec.outcome == 'failure'
381
+ run: |
382
+ gh issue create \
383
+ --label release-incident \
384
+ --title "Provenance backfill needed for ${VERSION}" \
385
+ --body "release-publish.yml reconcile job failed for ${VERSION}. Tag and publish completed successfully; only the provenance graph is incomplete. To recover, run \`cleo release reconcile ${VERSION} --backfill\` from a local checkout. Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
386
+ env:
387
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
388
+ timeout-minutes: 5