Repository navigation
400 lines (351 loc) · 13.6 KB
/
Copy pathci.yml
File metadata and controls
400 lines (351 loc) · 13.6 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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# This workflow is managed by gh actions-lock.
# SPDX-License-Identifier: MPL-2.0
# This workflow is managed by gh actions-lock.
# Main CI workflow for hypatia
# Tests Rust (adapters, cli, fixer, data) and the Elixir rules. The Haskell
# (registry) jobs below are dormant-gated on the tree's absence; the Logtalk
# rule engine was retired 2026-03-06 (absorbed into lib/rules/*.ex).
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
RUST_BACKTRACE: 1
jobs:
# ============================================================================
# Rust Jobs (adapters, cli, fixer, data)
# ============================================================================
rust-check:
name: Rust Check & Clippy
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: clippy, rustfmt
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2.9.3
with:
workspaces: ". -> target"
cache-on-failure: true
# Every cargo invocation in this workflow passes --locked: the committed
# Cargo.lock is a hard constraint, not a hint (#841). A failure reading
# "the lock file ... needs to be updated but --locked was passed" means
# Cargo.lock is out of date w.r.t. the manifests -- regenerate it
# deliberately and commit it; never drop --locked to get green.
- name: Run cargo check
run: cargo check --workspace --all-targets --locked
- name: Run clippy
run: cargo clippy --workspace --all-targets --locked -- -D warnings
rust-fmt:
name: Rust Format
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
rust-test:
name: Rust Tests
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [rust-check]
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2.9.3
with:
workspaces: ". -> target"
cache-on-failure: true
- name: Run tests
run: cargo test --workspace --all-targets --locked
- name: Run doc tests
run: cargo test --workspace --doc --locked
rust-test-coverage:
name: Rust Coverage
runs-on: ubuntu-latest
timeout-minutes: 60
needs: [rust-test]
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@v2.87.29
with:
tool: cargo-llvm-cov
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2.9.3
with:
workspaces: ". -> target"
cache-on-failure: true
- name: Generate coverage report
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
# ============================================================================
# Haskell Jobs (registry) — dormant-ready.
# The Haskell sub-project (registry/) is currently absent (its source was
# removed). `detect-haskell` gates the Haskell jobs on the presence of
# registry/hypatia.cabal so they SKIP cleanly while it is gone and
# re-activate automatically the moment it lands again. This realises the
# intent that was previously only described in a comment but never enforced,
# which left haskell-build and haskell-lint failing unconditionally.
# ============================================================================
detect-haskell:
name: Detect Haskell tree
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
present: ${{ steps.check.outputs.present }}
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Check for registry/hypatia.cabal
id: check
run: |
if [ -f registry/hypatia.cabal ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::registry/hypatia.cabal absent — Haskell build/lint jobs skipped (dormant)."
fi
haskell-build:
name: Haskell Build & Test
runs-on: ubuntu-latest
timeout-minutes: 60
needs: detect-haskell
if: needs.detect-haskell.outputs.present == 'true'
# registry/ is the Haskell sub-project; gate this job on its presence so
# PRs that don't touch (and don't ship) the Haskell tree don't fail here.
# The job re-activates the moment `registry/hypatia.cabal` lands.
defaults:
run:
working-directory: registry
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Setup Haskell
uses: haskell-actions/setup@v2.12.1
id: setup
with:
ghc-version: '9.6'
cabal-version: '3.10'
cabal-update: true
- name: Configure build
run: |
cabal configure --enable-tests --enable-benchmarks --disable-documentation
cabal build all --dry-run
- name: Restore cached dependencies
uses: actions/cache/restore@v6.1.0
id: cache
env:
key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
with:
path: ${{ steps.setup.outputs.cabal-store }}
key: ${{ env.key }}-plan-${{ hashFiles('registry/hypatia.cabal') }}
restore-keys: ${{ env.key }}-
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: cabal build all --only-dependencies
- name: Save cached dependencies
uses: actions/cache/save@v6.1.0
if: steps.cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.setup.outputs.cabal-store }}
key: ${{ steps.cache.outputs.cache-primary-key }}
- name: Build
run: cabal build all
- name: Run tests
run: cabal test all
- name: Check documentation
run: cabal haddock all
haskell-lint:
name: HLint
runs-on: ubuntu-latest
timeout-minutes: 60
needs: detect-haskell
if: needs.detect-haskell.outputs.present == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Setup HLint
uses: haskell-actions/hlint-setup@v2.4.10
with:
version: '3.8'
- name: Run HLint
uses: haskell-actions/hlint-run@v2.4.10
with:
path: registry/
fail-on: warning
# ============================================================================
# K9iser — validates the contract manifest and emits k9 artefacts.
# DOG-04 from testing-and-benchmarking/TESTING-TAXONOMY.adoc § VII.
#
# Validates k9iser.toml parses cleanly and every declared source path
# exists. If the `k9iser` CLI is on PATH, also runs `k9iser build`
# and uploads the generated artefacts. Absence of the CLI is a warning,
# not a failure — the parse + path check is the hard gate.
# ============================================================================
k9iser-validate:
name: k9iser manifest + build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Parse k9iser.toml + verify declared paths exist
run: |
set -euo pipefail
cargo build --release --manifest-path scripts/ci-tools/Cargo.toml \
--bin check-k9iser-paths
./scripts/ci-tools/target/release/check-k9iser-paths k9iser.toml
- name: Run k9iser build (if CLI available)
run: |
set -euo pipefail
if command -v k9iser >/dev/null 2>&1; then
k9iser build
ls -la generated/k9iser/ || true
else
echo "::warning::k9iser CLI not on PATH — skipping build step. Install from hyperpolymath/k9iser to enable full DOG-04 enforcement."
fi
- name: Upload k9 artefacts
if: hashFiles('generated/k9iser/**') != ''
uses: actions/upload-artifact@v7.0.2
with:
name: k9iser-artefacts
path: generated/k9iser/
retention-days: 14
# ============================================================================
# Elixir compile gate — runs the SAME toolchain the estate's dogfood gate uses
# ============================================================================
#
# Why this job exists (2026-10-25, hyperpolymath/secret-types PR #11):
#
# `lib/rules/actions_lock.ex` did not compile at all. Line 170 was
# `version in @supported_versions ->`, a guard expression used as a bare
# `case` clause head, which expands to a match against `:erlang.=:=/2`:
#
# error: cannot invoke remote function :erlang.=:=/2 inside a match
# └─ lib/rules/actions_lock.ex:170
# ** (CompileError) ... expanding macro: Kernel.in/2
#
# The break landed on 2026-10-20 in #927 and stayed on `main` for five days.
# Every "RSR oracle — dogfood this repo" job in the estate died on
# `mix deps.get && mix compile`, so ~400 consumer repositories lost their RSR
# scorecard without a single hypatia check going red for the right reason:
#
# * this workflow had no Elixir job at all — Rust and Haskell only;
# * tests.yml has `pull_request: null`, so its Elixir jobs never ran on a PR;
# * escript-soundness.yml does compile, but it was already red for unrelated
# reasons, so a hard CompileError was indistinguishable from the noise.
#
# A tree that cannot compile cannot run a test, so the regression test for
# this class of defect is the compile itself, on the versions the consumers
# actually use. Do not "fix" a red run here by dropping --warnings-as-errors
# or by moving the job behind a condition: that restores the silence.
elixir-compile:
name: Elixir Compile (dogfood toolchain)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
# Pinned to the exact versions in the estate dogfood gate
# (.github/workflows/dogfood-gate.yml in each consumer repo, job
# "RSR oracle — dogfood this repo"). Bump both together or the gate
# stops testing what consumers run.
- name: Setup Elixir/OTP (identical to the dogfood gate)
uses: erlef/setup-beam@v1.24.1
with:
elixir-version: '1.19.4'
otp-version: '28.3'
- name: Restore Mix cache
uses: actions/cache@v6.1.0
with:
path: |
deps
_build
key: mix-dogfood-${{ runner.os }}-${{ hashFiles('mix.lock') }}
restore-keys: |
mix-dogfood-${{ runner.os }}-
- name: Fetch dependencies
run: mix deps.get
- name: Compile (warnings are errors)
run: mix compile --warnings-as-errors
- name: Rule module regression tests
run: |
mix test \
test/rules/actions_lock_test.exs \
test/rules/actions_lock_closure_corpus_test.exs \
test/rules/rsr_conformance_test.exs \
test/research_extensions_test.exs \
test/deed_test.exs
# The consumers' invocation, verbatim: the oracle is called with an
# EXPLICIT --ssot. The default SSOT resolution is deliberately not
# exercised here — it depends on the layout of the checkout that hosts
# hypatia, which is the consumer's business, not this repo's. Passing the
# bundled fixture keeps the SSOT explicit, exactly as the gate does.
- name: RSR oracle smoke (consumer invocation, explicit SSOT)
run: |
mix hypatia.rsr_score . \
--ssot test/fixtures/a2ml/rsr-criteria-v2.a2ml
# ============================================================================
# Combined Status Check
# ============================================================================
ci-status:
name: CI Status
runs-on: ubuntu-latest
timeout-minutes: 30
needs:
- rust-check
- rust-fmt
- rust-test
- rust-test-coverage
- haskell-build
- haskell-lint
- k9iser-validate
- elixir-compile
if: always()
steps:
- name: Check all jobs status
run: |
if [[ "${{ needs.rust-check.result }}" == "failure" ]] || \
[[ "${{ needs.rust-fmt.result }}" == "failure" ]] || \
[[ "${{ needs.rust-test.result }}" == "failure" ]] || \
[[ "${{ needs.haskell-build.result }}" == "failure" ]] || \
[[ "${{ needs.haskell-lint.result }}" == "failure" ]] || \
[[ "${{ needs.k9iser-validate.result }}" == "failure" ]] || \
[[ "${{ needs.elixir-compile.result }}" == "failure" ]]; then
echo "One or more jobs failed"
exit 1
fi
echo "All CI jobs passed successfully"