-
Notifications
You must be signed in to change notification settings - Fork 0
278 lines (262 loc) · 11.8 KB
/
Copy pathci.yml
File metadata and controls
278 lines (262 loc) · 11.8 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
name: CI
# Every gate is its own job, and no gate depends on another. The gate matrix is evidence:
# five independent verdicts are more informative than one red run that hides four.
#
# This replaces a single `gates` job whose steps ran in sequence. When its test-ratchet
# step failed, credo, dialyzer and all three advisories were skipped and reported as `-`.
# That masked thirteen consecutive failures whose actual cause was one shell flag: the
# step ran under GitHub's default `shell: bash -e`, so `mix test` exiting non-zero aborted
# it before the baseline comparison could run. The gate logic now lives in tools/gate.sh,
# which both this workflow and .githooks/pre-commit call, so the two cannot diverge again.
on:
push:
# Every branch, not just main: a slice branch must gate before a PR exists.
branches: ["**"]
pull_request:
schedule:
# For the advisory job: new CVEs land against unchanged dependencies, so the audits
# need a clock.
#
# This used to add "NOT for the baseline gate -- that compares git-tracked values and a
# scheduled run would re-compare a commit its own push run already checked" (quoted in
# full: an earlier draft of this comment elided the middle clause, which is the half
# that is still true). Slice 16k falsified the rest in both halves, and
# review caught the stale sentence in the same diff that falsified it. First: the
# baseline job carries no `if:` excluding `schedule`, so it has always RUN on the
# nightly -- it simply measured nothing, which is the defect 16k fixes. Second, now
# that it measures, the comparison is not always a re-comparison: it compares
# `github.sha~1`, and after a rebase-merge of a multi-commit branch that is the
# branch's second-to-last commit, not the previous `main` tip, so the nightly's
# comparison is NARROWER than the push run's rather than a repeat of it.
#
# A consequence, derived and not yet observed here (this history is fully linear --
# `git rev-list --min-parents=2 --count --all` is 0): if a branch lowers a baseline in one
# commit and restores it in the next, the push run compares old-tip -> tip and sees no
# change, while the nightly compares those two commits and sees a raise. That would go
# red on `main`. It blocks no merge -- required checks are evaluated on a PR's own head.
# It is also self-clearing, which an earlier draft of this comment denied when it called
# it "noise nobody could clear": the next push to `main` moves `github.sha`, so the next
# nightly compares a different pair, and a `_corrections` entry clears it deliberately.
# Recorded here so the next person to see it has the mechanism rather than a mystery.
#
# Note: scheduled runs only fire from the default branch.
- cron: "17 6 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
MIX_ENV: test
# Written inside the workspace, not a tmpdir, so a failing gate's log can be uploaded.
# Previously a CI credo/dialyzer/test failure produced a count and nothing else: the
# log named in the error message lived in an ephemeral mktemp -d on a destroyed runner.
LOGDIR: ${{ github.workspace }}/gate-logs
jobs:
format:
name: Gate - format
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- run: ./tools/gate.sh format
compile:
name: Gate - compile (--warnings-as-errors)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- run: ./tools/gate.sh compile
secret-scan:
name: Gate - tracked secret-shaped files
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- run: ./tools/gate.sh secret-scan
test:
name: Gate - test ratchet
runs-on: ubuntu-latest
timeout-minutes: 20
# THE NAME IS HISTORICAL. Since slice 16 this gate is HARD-BLOCKING, not a ratchet: the
# baseline reached 0 in slice 13 and its entry was retired from .claude/gate-baseline.json,
# as that file's own _comment requires. tools/gate.sh scores it with hard_zero -- only 0
# passes, and there is no baseline to raise.
#
# The name stays because "Gate - test ratchet" is a REQUIRED STATUS CHECK in ruleset
# 22066749. Renaming a job renames its check context, and a required context that never
# reports stays pending forever, blocking every pull request. The rename is therefore a
# two-part change -- ruleset first, then this file -- and only the repository owner can
# edit the ruleset. Tracked in BACKLOG.md; see CLAUDE.md 4c.
#
# No postgres service: the default suite runs with HACKTUI_START_REPO unset and never
# connects. DB-backed tests are the `integration` job.
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- run: ./tools/gate.sh test
- if: always()
uses: actions/upload-artifact@v4
with:
name: gate-logs-test
path: gate-logs/
if-no-files-found: ignore
retention-days: 14
credo:
name: Gate - credo ratchet
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- run: ./tools/gate.sh credo
- if: always()
uses: actions/upload-artifact@v4
with:
name: gate-logs-credo
path: gate-logs/
if-no-files-found: ignore
retention-days: 14
dialyzer:
name: Gate - dialyzer ratchet
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- uses: actions/cache@v4
with:
path: priv/plts
key: ${{ runner.os }}-plt-otp28.1.1-ex1.19.2-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-plt-otp28.1.1-ex1.19.2-
- run: ./tools/gate.sh dialyzer
- if: always()
uses: actions/upload-artifact@v4
with:
name: gate-logs-dialyzer
path: gate-logs/
if-no-files-found: ignore
retention-days: 14
attestation:
name: Gate - attestation
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Re-derive each commit's diff and compare to its recorded claim
run: |
set +e
if [ -n "${{ github.base_ref }}" ]; then
# No --depth: checkout above already fetched full history, and --depth=0 is
# not a valid git value ("depth 0 is not a positive number"). The gate failed
# closed on that, which was correct, but it failed for the wrong reason.
git fetch origin "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" \
|| { echo "::error::cannot fetch base ref"; exit 1; }
# head.sha, NOT github.sha: on a pull_request event github.sha is the synthetic
# merge commit GitHub creates, which carries no Reviewed-diff trailer because
# no author wrote it.
./tools/gate.sh attestation "origin/${{ github.base_ref }}..${{ github.event.pull_request.head.sha }}"
elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ] || [ -z "${{ github.event.before }}" ]; then
./tools/gate.sh attestation "${{ github.sha }}~1..${{ github.sha }}"
else
./tools/gate.sh attestation "${{ github.event.before }}..${{ github.sha }}"
fi
mutation:
name: Gate - mutation harness
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
# Row 1 of the TSV is a CANARY whose pattern is deliberately absent. If the harness
# does not hard-abort on it, the harness is broken and no survivor count it reports
# is evidence.
- run: ./tools/gate.sh mutation
- if: always()
uses: actions/upload-artifact@v4
with:
name: gate-logs-mutation
path: gate-logs/
if-no-files-found: ignore
retention-days: 14
baseline:
name: Gate - baseline may only decrease
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compare against the ref this push started from
run: |
set +e
if [ -n "${{ github.base_ref }}" ]; then
git fetch --depth=1 origin \
"+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" \
|| { echo "::error::cannot fetch base ref; refusing to pass unmeasured"; exit 1; }
./tools/gate.sh baseline "origin/${{ github.base_ref }}"
elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ] || [ -z "${{ github.event.before }}" ]; then
# Same guard the attestation job carries above, reused rather than re-derived.
# `github.event.before` is ABSENT from the schedule and workflow_dispatch
# payloads, so this expanded to `baseline ""` and gate.sh matched it in the same
# branch as the all-zeros sentinel: "no previous ref (new branch); nothing to
# compare", return 0. Measured on run 34028977996 (schedule, main, 778accb8) --
# the gate reported success having compared zero keys, on a commit whose parent
# 268a83e8 carries the baseline file it declined to read. Nightly runs had been
# green on nothing since the schedule trigger was added.
#
# sha~1, not "nothing": on a multi-commit push this sees only the last commit,
# which is the HEAD~1 hole the else branch describes -- but a partial comparison
# is strictly more than none, and this branch is reached only when the payload
# gave us no better ref. gate.sh now REFUSES an empty ref outright, so this
# cannot silently regress to a pass.
./tools/gate.sh baseline "${{ github.sha }}~1"
else
# github.event.before, NOT HEAD~1. One run is created per push, at the tip, so
# HEAD~1 compares only the last commit -- a raise in an earlier commit of the
# same push is never seen, and no later run revisits it.
./tools/gate.sh baseline "${{ github.event.before }}"
fi
advisories:
name: "Advisory (non-blocking) - dependency audits"
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- run: ./tools/gate.sh advisories
integration:
name: "Advisory (non-blocking) - integration tests (DB-backed)"
runs-on: ubuntu-latest
timeout-minutes: 20
# Recorded residual: these 18 tests have never run to completion in CI. Removing
# continue-on-error is slice 16's work, once their real failure count is known.
continue-on-error: true
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: hacktui
POSTGRES_PASSWORD: postgres
POSTGRES_DB: hacktui_qualification_test
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 10s
--health-timeout 5s --health-retries 5
env:
HACKTUI_START_REPO: "true"
HACKTUI_DB_USER: hacktui
HACKTUI_DB_PASS: postgres
HACKTUI_DB_HOST: localhost
HACKTUI_DB_PORT: "5432"
HACKTUI_DB_NAME: hacktui_qualification_test
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/beam-setup
- run: mix ecto.create && mix ecto.migrate
- run: mix test --include integration