@cleocode/cleo 2026.5.78 → 2026.5.81
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.
- package/dist/cli/index.js +2795 -1092
- package/dist/cli/index.js.map +3 -3
- package/package.json +11 -11
- package/templates/workflows/README.md +157 -0
- package/templates/workflows/release-fanout.yml.tmpl +214 -0
- package/templates/workflows/release-prepare.yml.tmpl +296 -0
- package/templates/workflows/release-publish.yml.tmpl +388 -0
- package/templates/workflows/release-rollback.yml.tmpl +322 -0
|
@@ -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
|