-
Notifications
You must be signed in to change notification settings - Fork 0
327 lines (308 loc) · 13.7 KB
/
release.yml
File metadata and controls
327 lines (308 loc) · 13.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
327
name: release
# Phase 6 Plan 06-04 — full 4-gate release pipeline + build + publish.
# Replaces the Phase 1 single-job scaffold. See 06-RESEARCH.md Pattern 6 and
# .planning/phases/06-distribution-release-pipeline/06-04-PLAN.md for the
# authoritative design. Gate sequence is enforced by `needs:` — any red gate
# short-circuits the pipeline before publish.
#
# Gates in order:
# gate-1-precommit (pre-commit + CLI unit tests — cheapest/fastest)
# gate-2-docker (tests/docker/run.sh × {ubuntu-22.04, ubuntu-24.04}
# — includes 51-*.bats → TST-05 inside Docker)
# gate-3-qemu (tests/qemu/boot.sh × {22.04, 24.04} with /dev/kvm
# — includes 51-*.bats → TST-05 inside QEMU)
# gate-4-pinned-combo (tests/docker/run.sh ubuntu-24.04 with pinned catalog
# combo — 50-agents.bats + 51-*.bats → TST-08)
# build (scripts/build-release.sh <tag> — tarball + .sha256
# + catalog snapshot + optional .deb; uploads dist/)
# publish (softprops/action-gh-release@v2.6.2 — Node-20 pin;
# only runs on tag push, skipped on workflow_dispatch)
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. v0.3.0). Dry-runs skip the publish job.'
required: true
# Pitfall 9 mitigation — never cancel an in-progress release. A second tag push
# queues behind the first; both runs complete and re-publish byte-identical
# artifacts (reproducible build in Plan 06-01). cancel-in-progress would risk a
# half-uploaded Release page.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
# Default read-only for all jobs; publish overrides to contents: write.
permissions:
contents: read
jobs:
# Gate 0 — resolve the tag from either push (refs/tags/vX.Y.Z) or
# workflow_dispatch input. Validates the vX.Y.Z[-suffix] shape once so
# every downstream job inherits the same canonical value.
resolve:
runs-on: ubuntu-24.04
outputs:
tag: ${{ steps.tag.outputs.value }}
steps:
- uses: actions/checkout@v5
- name: Resolve and validate tag
id: tag
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
tag="${INPUT_TAG:-${GITHUB_REF##*/}}"
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "::error::invalid tag '$tag' — must match vX.Y.Z[-suffix]"
exit 1
fi
echo "value=$tag" >> "$GITHUB_OUTPUT"
echo "resolved release tag: $tag"
# Gate 1 — pre-commit + CLI unit tests. Fastest gate; fails cheapest on
# trivial drift (formatting, lint, schema). Mirrors test.yml's pre-commit +
# cli-unit jobs but inlined here so a tag push is self-contained and does
# not race with a branch-triggered test.yml.
gate-1-precommit:
needs: resolve
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install pre-commit
run: pip install pre-commit==4.0.1
# The catalog-schema-validate hook runs `node plugin/cli/scripts/
# validate-catalog.mjs` which imports `ajv`. Without these deps the hook
# fails with "Cannot find package 'ajv'". Install via pnpm (project's
# lockfile is pnpm-lock.yaml) so the hook can resolve its imports.
- name: Enable pnpm via corepack
run: corepack enable
- name: Install plugin/cli deps (ajv for catalog-schema-validate hook)
working-directory: plugin/cli
run: pnpm install --frozen-lockfile
- name: Run pre-commit on all files
run: pre-commit run --all-files --show-diff-on-failure
# Use pnpm (canonical lockfile); deps already installed in the step above.
# CLAUDE.md convention is `cd plugin/cli && pnpm test`. Steps are split so
# tsc compile errors are visible separately from test execution failures.
- name: Compile CLI tests
working-directory: plugin/cli
run: pnpm exec tsc -p tsconfig.test.json
- name: Verify test compilation
working-directory: plugin/cli
run: |
set -euxo pipefail
if [ ! -d dist-test/test ] || [ -z "$(ls -A dist-test/test 2>/dev/null)" ]; then
echo "::error::dist-test/test missing or empty after tsc compile"
ls -la dist-test 2>/dev/null || echo "(dist-test does not exist)"
exit 1
fi
ls dist-test/test/
# Use explicit *.test.js glob — Node v22+ does not auto-discover test
# files when given a bare directory path; it tries to resolve the dir as
# a module and fails with MODULE_NOT_FOUND. Glob expansion at the shell
# level passes explicit file args, which works universally.
- name: Run CLI unit tests
working-directory: plugin/cli
shell: bash
run: pnpm exec node --test dist-test/test/*.test.js
# Gate 2 — Docker matrix. Runs tests/docker/run.sh on every supported Ubuntu
# LTS image. The Docker harness executes the full tests/bats/ suite INCLUDING
# tests/bats/51-agt02-release-gate.bats — this satisfies TST-05's
# "AGT-02 inside Docker" half. fail-fast:false so any arm failing still
# shows the others' signal for faster triage. AGE-11 added 26.04 on
# 2026-04-26.
gate-2-docker:
needs: gate-1-precommit
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
ubuntu: [ubuntu-22.04, ubuntu-24.04, ubuntu-26.04]
steps:
- uses: actions/checkout@v5
- name: Docker bats matrix (incl. 51-*.bats — TST-05 inside Docker)
run: bash tests/docker/run.sh ${{ matrix.ubuntu }}
# Gate 3 — QEMU matrix. Release-gate-only (ADR-007 — Docker alone is
# disqualified). Mirrors nightly-qemu.yml's KVM udev + image-cache pattern.
# boot.sh runs the full bats suite inside the guest INCLUDING
# tests/bats/51-*.bats — satisfies the "AGT-02 inside QEMU" half of TST-05.
gate-3-qemu:
needs: gate-2-docker
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
ubuntu: ['22.04', '24.04', '26.04']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '22'
# Pitfall 4 mitigation: ubuntu-24.04 runners ship /dev/kvm but the runner
# user is NOT in the kvm group. A udev rule with MODE=0666 exposes the
# device without group membership. Fail fast if the device is still
# inaccessible — TCG fallback would silently blow past the timeout.
- name: Enable /dev/kvm access (Pitfall 4)
run: |
set -euo pipefail
sudo bash -c 'echo "KERNEL==\"kvm\", GROUP=\"kvm\", MODE=\"0666\"" > /etc/udev/rules.d/99-kvm.rules'
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# `udevadm trigger` is async — perms may not be applied immediately.
# Wait for the trigger to settle, then poll up to 5s before failing.
sudo udevadm settle --timeout=10 || true
for _ in 1 2 3 4 5 6 7 8 9 10; do
if [[ -r /dev/kvm && -w /dev/kvm ]]; then
break
fi
sleep 0.5
done
if [[ ! -r /dev/kvm || ! -w /dev/kvm ]]; then
echo "::error::/dev/kvm not accessible after udev rule — KVM required"
ls -la /dev/kvm || true
exit 1
fi
- name: Install QEMU + cloud-image-utils
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
qemu-system-x86 \
cloud-image-utils \
netcat-openbsd \
jq
# Cache keyed on tests/qemu/cloud-images.txt so rotating an upstream
# image URL forces a fresh fetch. boot.sh ALSO re-verifies sha256 against
# upstream SHA256SUMS on every run (Pitfall 10 — belt-and-suspenders:
# cache poisoning is rejected even if the cache key hash collided).
- name: Cache Ubuntu cloud images
uses: actions/cache@v4
with:
path: ~/.cache/agentlinux/qemu
key: cloud-image-${{ matrix.ubuntu }}-${{ hashFiles('tests/qemu/cloud-images.txt') }}
restore-keys: |
cloud-image-${{ matrix.ubuntu }}-
- name: QEMU boot + installer + bats (incl. 51-*.bats — TST-05 inside QEMU)
run: bash tests/qemu/boot.sh ${{ matrix.ubuntu }}
# Artifacts on failure only — serial.log is the primary diagnostic when
# cloud-init or the installer hangs. 14-day retention keeps storage
# bounded; preserving green-run logs wastes storage and hides signal.
- name: Upload QEMU artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: qemu-artifacts-${{ matrix.ubuntu }}
path: tests/qemu/artifacts/
if-no-files-found: ignore
retention-days: 14
# Gate 4 — pinned-combo (TST-08). Exercises the full pinned catalog on
# Ubuntu 24.04 Docker (our pinned dev env): every catalog agent installed at
# its pinned_version + tests/bats/50-agents.bats + tests/bats/51-*.bats.
# tests/docker/run.sh already drives this end-to-end (Phase 4+5 provisioners
# install the pinned combo). Re-running it here as a named gate makes the
# TST-08 signal observable as a distinct green box in the Actions UI.
gate-4-pinned-combo:
needs: gate-3-qemu
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- name: Pinned catalog combo + 50-agents.bats + 51-*.bats (TST-08)
run: bash tests/docker/run.sh ubuntu-24.04
# Build — only after all 4 gates green. Reproducible tarball + sha256 sidecar
# + catalog snapshot + optional .deb (SKIP_DEB=1 if fpm install fails).
# Uploads dist/ as an Actions artifact so the publish job can download it
# without re-running the build (matches download-artifact@v4 major).
build:
needs: [resolve, gate-4-pinned-combo]
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '22'
# fpm is optional per ADR-006 — .deb is a nice-to-have for v0.3.0, not a
# release-blocker. Install failure falls back to SKIP_DEB=1 with a
# ::warning so the build proceeds with just the tarball + sha256 +
# catalog snapshot. build-release.sh honors SKIP_DEB.
- name: Install fpm (optional .deb — ADR-006, Pitfall 6)
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends ruby-dev build-essential
if ! sudo gem install --no-document fpm; then
echo "::warning::fpm install failed — proceeding with SKIP_DEB=1 (.deb will not be published)"
echo "SKIP_DEB=1" >> "$GITHUB_ENV"
fi
- name: Build release artifacts
env:
TAG: ${{ needs.resolve.outputs.tag }}
run: bash scripts/build-release.sh "$TAG"
# T-06-08 mitigation: explicit verify step. The release MUST have its
# .sha256 sibling; the catalog snapshot MUST exist. sha256sum -c round-
# trips the tarball against its sidecar before upload, so a corrupted
# write is caught here, not by a user running curl | bash.
- name: Verify release artifact assembly (T-06-08, Pitfall 8)
env:
TAG: ${{ needs.resolve.outputs.tag }}
run: |
set -euo pipefail
if [[ ! -s "dist/agentlinux-${TAG}.tar.gz" ]] \
|| [[ ! -s "dist/agentlinux-${TAG}.tar.gz.sha256" ]] \
|| [[ ! -s "dist/catalog-${TAG}.json" ]]; then
echo "::error::release artifact assembly failed — missing required files in dist/"
ls -la dist/ || true
exit 1
fi
( cd dist && sha256sum -c "agentlinux-${TAG}.tar.gz.sha256" )
- name: Upload release artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: dist/
if-no-files-found: error
retention-days: 7
# Publish — only on tag push (not workflow_dispatch). softprops/
# action-gh-release@v2.6.2 is the last Node-20-compatible release per
# 06-RESEARCH.md Standard Stack; v3 requires Node-24 and will be the next
# upgrade after Actions' Node-24 migration completes.
#
# The files glob uploads every asset type produced by the build:
# - agentlinux-*.tar.gz (canonical distribution tarball)
# - agentlinux-*.tar.gz.sha256 (INST-03 — required sibling for curl-installer)
# - catalog-*.json (CAT-05 — versioned catalog snapshot)
# - agentlinux_*.deb (optional — absent if SKIP_DEB=1; glob tolerates)
publish:
needs: [resolve, build]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts
path: dist/
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2.6.2
with:
tag_name: ${{ needs.resolve.outputs.tag }}
generate_release_notes: true
fail_on_unmatched_files: false
files: |
dist/agentlinux-*.tar.gz
dist/agentlinux-*.tar.gz.sha256
dist/catalog-*.json
dist/agentlinux_*.deb
dist/VERSION