-
Notifications
You must be signed in to change notification settings - Fork 1
545 lines (516 loc) · 27.1 KB
/
Copy pathci.yml
File metadata and controls
545 lines (516 loc) · 27.1 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
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
name: CI
on:
push:
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened, edited]
branches: [develop, main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
jobs:
semantic-pr:
name: Semantic PR
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
chore
revert
requireScope: false
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo test --all-targets
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo clippy --all-targets -- -D warnings
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all --check
# M-2 (adversary finding): gate the cargo-fuzz harness compilation so a
# broken harness is not silently ignored by the existing stable-toolchain
# jobs. AC-011 requires `cargo +nightly fuzz build fuzz_decode_packet` to
# succeed. This is a build-only check — the fuzzer is never executed in CI.
fuzz-build:
name: Fuzz build
runs-on: ubuntu-latest
# Nightly toolchain and cargo-fuzz are pinned together to a known-good
# combination. The toolchain is pinned to nightly-2026-05-21
# (rustc 1.97.0-nightly, confirmed green on develop 2026-05-22).
# cargo-fuzz 0.13.1 requires rustix; older rustix (0.36.5) used a
# rustc_layout_scalar_valid_range_start attribute that was removed from
# nightly in early 2026, causing build failures (PR #110, runs
# 26296832804 and 26296980821). The version below resolves a new-enough
# rustix automatically via --no-locked, which is safe once the toolchain
# is fixed. To bump: pick a newer known-good nightly date, verify the
# fuzz build passes, then update the date here. Do NOT enable renovatebot
# or Dependabot auto-update for this pin — bumping is a deliberate,
# periodic maintenance action, not automatic.
timeout-minutes: 25
env:
# The fuzz sub-crate builds under nightly with libfuzzer-sys flags that
# are incompatible with -Dwarnings. Override the workflow-level env for
# this job only; the stable-toolchain jobs are unaffected.
RUSTFLAGS: ""
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dtolnay/rust-toolchain@nightly
with:
# Pin to a specific, known-good dated nightly. See job comment above.
toolchain: nightly-2026-05-21
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Use a separate cache key so the nightly fuzz build does not
# pollute the stable-toolchain cache shared by test/clippy/fmt.
key: fuzz-nightly-2026-05-21
- name: Install cargo-fuzz
# Pin to 0.13.1 (the version validated against nightly-2026-05-21).
# Do NOT use --locked: cargo-fuzz 0.13.1's Cargo.lock pins
# rustix 0.36.5 which uses rustc_layout_scalar_valid_range_start, an
# attribute removed from nightly. Without --locked, cargo resolves a
# newer compatible rustix that compiles cleanly on this toolchain.
run: cargo install cargo-fuzz --version 0.13.1
- name: Build fuzz harness (compile check only)
run: cargo +nightly-2026-05-21 fuzz build fuzz_decode_packet
- name: Verify fuzz target is listed (positive coverage assertion)
run: cargo +nightly-2026-05-21 fuzz list | grep -qx fuzz_decode_packet
# LESSON-P2.06: supply-chain scanning split into two complementary
# jobs so a noisy advisory in one source does not block merges on
# an unrelated regression.
audit:
name: Audit
runs-on: ubuntu-latest
# Advisory database may surface new findings between code changes,
# so this job is informational on PRs (continue-on-error: true)
# and run on a schedule against develop to catch zero-days.
#
# Implementation note: rustsec/audit-check@v2.0.0 was replaced with
# a direct `cargo audit` shell invocation. The action required
# `checks: write` permission to post GitHub Checks API annotations,
# which the default GITHUB_TOKEN does not provide, causing:
# "Resource not accessible by integration" (exit 1)
# on every push run even when no vulnerabilities were found.
# Using plain cargo audit avoids the API call entirely.
continue-on-error: true
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Run cargo audit
# RUSTSEC-2025-0119 (number_prefix unmaintained) was resolved by bumping
# indicatif 0.17→0.18, which replaced number_prefix with unit-prefix.
# RUSTSEC-2026-0097 (rand 0.8.5 unsound): rand does not appear as a
# direct wirerust dependency; it is pulled transitively via the chain
# wirerust → tls-parser → phf_generator → rand 0.8.x. tls-parser uses
# phf_generator to build its compile-time hash tables, and phf_generator
# depends on rand for its hash-key generation. Resolved by bumping rand
# 0.8.5 → 0.8.6 via `cargo update -p rand` (maint-2026-06-22, PR #304).
run: cargo audit
deny:
name: Deny
runs-on: ubuntu-latest
# cargo-deny enforces the license / banned-deps / sources policy
# in `deny.toml`. Failures are blocking — these are
# repo-policy violations, not external advisory noise.
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
with:
command: check bans licenses sources
# W11-D2 / F-W16-WAVE-P2-003: Test-seam trust-boundary gate.
#
# Policy: functions suffixed `_for_testing` are test-seam helpers and MUST
# only appear as definitions inside src/. They must NEVER be called from
# production code paths in src/. Test callers live in tests/ (integration
# tests) or inside `#[cfg(test)]` modules; those paths are NOT scanned here.
#
# How it works:
# 1. grep -rn "_for_testing(" src/ — find every occurrence in src/
# 2. grep -v "fn [a-zA-Z_]*_for_testing(" — strip definition lines
# (lines of the form `fn some_name_for_testing(` are the seam definitions
# themselves; `pub fn`, `async fn`, indented `fn`, etc. all contain `fn `
# followed by the identifier, so they all match and are excluded)
# 3. If any lines survive the filter they are production call-sites —
# a violation. The step captures them and fails with a clear message.
#
# Baseline: zero violations on develop as of 2026-05-28 (drift item W11-D2).
trust-boundary:
name: Trust-boundary (test-seam gate)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Verify no production callers of _for_testing seam functions
# Scans src/ only. tests/ and #[cfg(test)] blocks are test territory
# and are intentionally excluded — they are the legitimate callers.
# Fails CI if any production call-site is found.
shell: bash
run: |
set -euo pipefail
# AC-158-004: SEC-001-style src/ existence guard (mirrors help-provenance-gate).
# If src/ is renamed or deleted, grep exits 2 and || true would suppress it,
# yielding a false PASS. The guard fires first so misconfiguration is loud.
if ! test -d src/; then
echo "FAIL: trust-boundary: src/ directory not found — seam scan target moved?"
echo "Update the scan target in .github/workflows/ci.yml before merging."
exit 1
fi
VIOLATIONS=$(grep -rn "_for_testing(" src/ | grep -v "fn [a-zA-Z_]*_for_testing(") || true
if [ -n "${VIOLATIONS}" ]; then
echo "FAIL: Production call-sites of _for_testing seam functions found in src/:"
echo "${VIOLATIONS}"
echo ""
echo "Functions suffixed _for_testing are test-seam helpers and must not"
echo "be called from production code. Move callers into tests/ or"
echo "#[cfg(test)] modules, or refactor the seam."
exit 1
fi
echo "PASS: No production callers of _for_testing functions found in src/"
# Help-provenance gate: prevent internal factory IDs from leaking into user-facing
# --help text rendered by clap from `///` doc-comments.
#
# Background: clap derives --help output exclusively from `///` (doc-comment) lines
# on Parser/Args/Subcommand/ValueEnum structs and their fields. Plain `//` comments
# are never rendered to users and are not scanned here. Internal identifiers of the
# factory convention must not appear in `///` doc-comments in clap-hosting files —
# they are implementation bookkeeping and have no meaning to end users.
#
# Scope: src/cli.rs only. All clap derive macros (#[derive(Parser)], #[arg(...)],
# #[command(...)]) currently live in src/cli.rs. Other src/ files use `///` for
# rustdoc on internal/library types (findings.rs, decoder.rs, etc.) where internal
# ID traceability is legitimate developer documentation and must not be stripped.
# If a new clap-derive module is added to src/, append its path to the grep
# target in the step below.
#
# Pattern rationale:
# \b[A-Z]{2,}-[0-9A-Z] — matches any factory internal-ID convention:
# two-or-more uppercase letters, a literal hyphen, then a digit or uppercase
# letter. This covers the full known set of factory prefixes:
# BC- behavioral contract refs (e.g. BC-2.11.028, BC-2.14.023)
# STORY- story refs (e.g. STORY-114, STORY-119)
# LESSON- lesson refs (e.g. LESSON-P1.03, LESSON-P2.05)
# VP- variant-proposal refs (e.g. VP-016)
# ADR- architecture decision refs (e.g. ADR-0003)
# EC- evaluation criterion refs (e.g. EC-006)
# AC- acceptance criterion refs (e.g. AC-007)
# TD- tech-debt refs (e.g. TD-012)
# PG- policy-gate refs (e.g. PG-001)
# and any future two-plus-letter prefix that follows the same convention.
# False-positive safety: ordinary hyphenated English prose uses lowercase
# (e.g. "non-zero", "opt-in") and will never match [A-Z]{2,}. Legitimate
# uppercase terms in help text that do not precede a hyphen (e.g. "MITRE",
# "JSON", "TCP", "ARP") are equally safe — the hyphen-then-[0-9A-Z] suffix
# anchors the match to ID-structured tokens only.
#
# Standards-body exclusion (SEC-002 / CWE-697):
# The broad regex also matches standards-body identifiers (RFC-9293, ISO-27001,
# CVE-2024-1234, IEC-62443, ANSI-X9, NIST-800, IEEE-802) which are legitimate
# and useful in help text. A second-pass filter strips these known prefixes so
# that legitimate standards references never false-positive the gate.
# Factory IDs use short, project-specific prefixes (BC, STORY, LESSON, VP, ADR,
# EC, AC, TD, PG) that do not appear in the exclusion list.
#
# File-not-found guard (SEC-001 / CWE-390):
# If src/cli.rs is renamed or deleted, `grep … || true` would silently return
# an empty VIOLATIONS string and falsely PASS. An explicit existence check
# before grep ensures such structural changes fail loudly so the gate scope
# can be updated to match the new clap-derive module location.
#
# Only `///` lines (doc-comments) are matched; `//` comment lines are excluded by
# the leading `^\s*///` anchor.
help-provenance-gate:
name: Help-provenance gate (no internal IDs in clap /// doc-comments)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Verify no internal factory IDs in clap /// doc-comments
# Scans src/cli.rs (the sole clap-derive module) for `///` lines containing
# factory internal-ID patterns. The pattern \b[A-Z]{2,}-[0-9A-Z] catches any
# UPPERCASE-prefix factory ID (BC-, STORY-, LESSON-, VP-, ADR-, EC-, AC-,
# TD-, PG-, and future additions) while excluding lowercase hyphenated prose
# and standalone uppercase acronyms that are not followed by -[digit/UPPER].
# These patterns appear verbatim in clap --help output and must not be visible
# to end users. Plain // comment lines are not matched and are never rendered
# by clap. If a new clap-derive module is added, append its path to the grep target.
#
# SEC-001 (CWE-390): explicit existence check before grep prevents a renamed or
# deleted src/cli.rs from silently returning an empty match and false-passing.
# SEC-002 (CWE-697): standards-body IDs (RFC, ISO, CVE, IEC, ANSI, NIST, IEEE)
# are excluded via a second-pass filter so legitimate references in help text
# never false-positive. Factory prefixes (BC, STORY, VP, ADR, etc.) are disjoint
# from this exclusion list and continue to be caught correctly.
shell: bash
run: |
set -euo pipefail
# SEC-001: fail loudly if the scan target has been renamed or removed.
# Without this check, `grep … || true` would silently PASS on a missing file
# (grep exits 2 for file-not-found, which `|| true` swallows). Updating the
# gate scope to match a new clap-derive module location is a deliberate step.
if ! test -f src/cli.rs; then
echo "FAIL: help-provenance-gate: src/cli.rs not found — clap surface moved?"
echo "Update the gate target in .github/workflows/ci.yml to match the new"
echo "clap-derive module location before merging."
exit 1
fi
# SEC-002: strip known standards-body IDs from grep hits before deciding
# pass/fail. RFC-NNNN, ISO-NNNNN, CVE-YYYY-NNNN, IEC-NNNNN, ANSI-XXX,
# NIST-NNN, IEEE-NNN are legitimate references in public help text and must
# not trigger the gate. Factory IDs (BC-, STORY-, LESSON-, VP-, ADR-, EC-,
# AC-, TD-, PG-) are not in this exclusion list and continue to be caught.
VIOLATIONS=$(
grep -n -E '^\s*///.*\b[A-Z]{2,}-[0-9A-Z]' src/cli.rs \
| grep -vE '\b(RFC|ISO|CVE|IEC|ANSI|NIST|IEEE)-' \
|| true
)
if [ -n "${VIOLATIONS}" ]; then
echo "FAIL: Internal factory IDs found in /// doc-comments in src/cli.rs:"
echo "${VIOLATIONS}"
echo ""
echo "/// doc-comments in clap-derive files are rendered verbatim into"
echo "--help output and must not contain internal IDs (BC-NNN, STORY-NNN,"
echo "LESSON-*, VP-NNN, ADR-NNN, EC-NNN, AC-NNN, TD-NNN, etc.)."
echo "Move internal references to plain // comments, which are"
echo "never rendered by clap and are invisible to end users."
exit 1
fi
echo "PASS: No internal factory IDs found in /// doc-comments in src/cli.rs"
# Supply-chain hardening: enforce that every remote GitHub Action `uses:` reference
# is pinned to a 40-character commit SHA rather than a mutable tag or branch.
# Mutable refs (e.g. @v6, @v2.9.1, @stable) can be silently moved by the upstream
# maintainer, allowing a compromised tag to execute arbitrary code in CI.
#
# Documented exemption: dtolnay/rust-toolchain@stable and @nightly are intentionally
# exempt. The rust-toolchain action is a channel-selection installer whose entire
# purpose is to track the rolling stable/nightly channel; pinning it to a SHA would
# defeat that purpose. These two refs are tracked for separate resolution and are
# explicitly allowlisted in the gate script below.
action-pin-gate:
name: Action pin gate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Verify all remote actions are SHA-pinned
shell: bash
run: |
set -euo pipefail
# Allowlisted branch-ref actions (documented exemptions only).
# dtolnay/rust-toolchain@stable and @nightly: channel-selection installer;
# pinning to SHA would defeat its purpose of tracking rolling toolchain channels.
ALLOWLIST="dtolnay/rust-toolchain@stable dtolnay/rust-toolchain@nightly"
# PG-W71-CI-SCAN-GUARDS / SEC-001: scan-target existence guard.
# Mirrors the trust-boundary and help-provenance-gate patterns.
# If .github/workflows/ is renamed or emptied, the grep loop processes zero
# files and trivially PASSes — an explicit guard ensures structural changes
# fail loudly so the scope can be updated intentionally before merging.
if ! test -d .github/workflows/; then
echo "FAIL: action-pin-gate: .github/workflows/ directory not found — scan target moved?"
echo "Update the scan target in .github/workflows/ci.yml before merging."
exit 1
fi
yml_count=$(find .github/workflows/ -maxdepth 1 -name "*.yml" | wc -l)
if [ "${yml_count}" -eq 0 ]; then
echo "FAIL: action-pin-gate: no *.yml files found in .github/workflows/ — scan target empty?"
echo "Update the scan target in .github/workflows/ci.yml before merging."
exit 1
fi
FAILURES=0
VALIDATED=0
SHA_RE='^[0-9a-f]{40}$'
while IFS= read -r line; do
# Extract FILE:LINENO:CONTENT from grep output
file_line="${line%%:*}"
rest="${line#*:}"
lineno="${rest%%:*}"
content="${rest#*:}"
# Pull the uses: value — strip everything up to and including 'uses:', trim trailing whitespace/comment.
# Uses greedy '.*uses:' to handle '- uses:' and 'uses:' forms equally.
ref_full=$(echo "${content}" | sed 's/.*uses:[[:space:]]*//' | sed 's/[[:space:]].*//')
# Skip local composite actions (uses: ./...)
case "${ref_full}" in
./*) continue ;;
esac
# Skip if it matches the allowlist
skip=0
for allowed in ${ALLOWLIST}; do
if [ "${ref_full}" = "${allowed}" ]; then
skip=1
break
fi
done
[ "${skip}" -eq 1 ] && continue
# PG-W71-CI-SCAN-GUARDS: count each non-local, non-allowlisted ref toward the
# positive-coverage assertion below. A zero count means the scan found no remote
# action refs to validate — suspicious even if FAILURES is also zero.
VALIDATED=$((VALIDATED + 1))
# Extract the ref token (after the @)
ref_token="${ref_full##*@}"
# PASS only if it is a 40-char lowercase hex SHA
if ! echo "${ref_token}" | grep -qE "${SHA_RE}"; then
echo "FAIL [${file_line}:${lineno}]: '${ref_full}' uses a mutable ref '${ref_token}' — must be a 40-char commit SHA"
FAILURES=$((FAILURES + 1))
fi
done < <(grep -rn "uses:[[:space:]]*[A-Za-z0-9._-]*/[A-Za-z0-9._-]*@" .github/workflows/*.yml)
if [ "${FAILURES}" -gt 0 ]; then
echo ""
echo "Found ${FAILURES} action(s) with mutable refs."
echo "Pin each action to a full 40-character commit SHA with a # vX.Y.Z comment for readability."
echo "To exempt an action, add it to the ALLOWLIST in this script with a justification comment."
exit 1
fi
# Positive-coverage assertion (PG-W71-CI-SCAN-GUARDS): zero validated refs means
# no 'uses:' lines survived the filter — the gate produced no signal. A stripped
# workflow tree cannot bypass the check by containing no remote action refs at all.
if [ "${VALIDATED}" -eq 0 ]; then
echo "FAIL: action-pin-gate: zero remote action refs validated — 'uses:' lines missing from scan target?"
echo "If all workflows were intentionally removed, update or disable this gate explicitly."
exit 1
fi
echo "PASS: ${VALIDATED} remote action ref(s) validated, 0 mutable."
# DF-GREEN-DOC-TENSE-SWEEP gate: detect stale RED-phase comment headers in test files.
#
# Problem: during strict TDD, test files receive module-level or section-level comments
# asserting that tests "MUST FAIL" or "are designed to FAIL (Red Gate)". After the
# implementation ships, these comments become stale — the test bodies contain real GREEN
# assertions but the headers still claim RED stub behaviour. This pattern recurred on all
# 4 stories of feature-enip-v0.11.0, prompting a mechanical gate (DF-GREEN-DOC-TENSE-SWEEP).
#
# Implementation: bin/check-green-doc-tense scans tracked tests/*.rs and src/**/*.rs
# comment lines for a curated set of current-state RED assertions. It exits 1 with a
# file:line + token message per violation, and exits 0 when clean. A self-test
# (bin/test_check_green_doc_tense.py) verifies a fixture suite of known-bad patterns
# are flagged and known-good patterns (past-tense provenance, inline test assertions,
# statistical descriptions) are not, so the gate itself is tested. Pure Python 3, no
# dependencies.
#
# The self-test is run first so a broken gate script surfaces immediately, not after a
# false-negative on the scan.
green-doc-tense-gate:
name: Green-doc-tense gate (DF-GREEN-DOC-TENSE-SWEEP)
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Self-test the gate script
run: python3 bin/test_check_green_doc_tense.py
- name: Scan for stale RED-phase comment headers in test files
run: python3 bin/check-green-doc-tense
# AC-165-001 / PG-W74-CI-BIN-SELFTEST: run Python self-test suites for bin/ gate scripts.
#
# STORY-164 (wave-74) delivered bin/test_validate_citations.py and
# bin/test_changelog_gate_content.py without CI wiring. This job closes that gap
# following the green-doc-tense-gate structural pattern (STORY-162 AC-162-002).
# W75 NIT-1: step names are count-free -- suite sizes grow over time and a
# hardcoded count would silently stale (same rationale as the
# green-doc-tense-gate job above).
bin-selftest:
name: Bin selftest suites
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Run bin/test_validate_citations.py
run: python3 bin/test_validate_citations.py
- name: Run bin/test_changelog_gate_content.py
run: python3 bin/test_changelog_gate_content.py
- name: Run bin/test_gitignore_mutants_glob.py
run: python3 bin/test_gitignore_mutants_glob.py
# AC-158-001 / PG-W71-CHANGELOG: enforce CHANGELOG entry for production-code PRs.
#
# Trigger set: src/ (production Rust), Cargo.toml (dependency/version changes),
# bin/ (factory tooling shipped with the repo). All three are user-visible surfaces
# that warrant a CHANGELOG entry on change.
#
# Excluded surfaces (by design, documented here per AC-158-001):
# tests/ — process-internal; test-only changes do not alter user-visible behavior
# .github/ — process-internal; CI config changes are not product behavior changes
# docs/ — self-documenting; ADR/README authoring does not describe product changes
# Cargo.lock — excluded: transitive-dep bumps route through maintenance-sweep
# CHANGELOG discipline, not through this per-PR gate
#
# Runs on pull_request events targeting develop only (AC-158-001(a) literal scope:
# release/hotfix PRs to main are outside the gate; push-to-develop is inherently
# no-op — origin/develop == HEAD, so the diff is always empty on direct pushes).
# F-S158-P1-003: github.base_ref == 'develop' added to restrict to develop PRs.
changelog-gate:
name: CHANGELOG gate (AC-158-001, PG-W71-CHANGELOG)
if: github.event_name == 'pull_request' && github.base_ref == 'develop'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Check CHANGELOG.md is updated when src/, Cargo.toml, or bin/ changes
shell: bash
run: |
set -euo pipefail
# fetch-depth: 0 in the checkout step already provides origin/develop.
CHANGED=$(git diff --name-only origin/develop...HEAD)
# Check whether this PR touches the CHANGELOG-gate trigger set
# F-S158-P1-004: Cargo\.toml$ anchored to prevent Cargo.toml.bak spurious match.
TRIGGERS=$(echo "${CHANGED}" | grep -E '^(src/|Cargo\.toml$|bin/)' || true)
if [ -z "${TRIGGERS}" ]; then
echo "PASS: no files in CHANGELOG-gate trigger set changed (src/, Cargo.toml, bin/)."
exit 0
fi
# Trigger set was hit — CHANGELOG.md must also be in the diff
if echo "${CHANGED}" | grep -q '^CHANGELOG\.md$'; then
# AC-164-003: content assertion via bin/changelog-gate-check.
# A whitespace-only touch must not satisfy the gate.
git diff origin/develop...HEAD -- CHANGELOG.md | bin/changelog-gate-check
exit 0
fi
echo "FAIL: AC-158-001 / PG-W71-CHANGELOG — this PR modifies files in the"
echo "CHANGELOG-gate trigger set (src/, Cargo.toml, or bin/) but does not"
echo "include a CHANGELOG.md update."
echo ""
echo "Trigger-set files changed:"
echo "${TRIGGERS}"
echo ""
echo "Add an [Unreleased] entry to CHANGELOG.md describing the change."
echo "(Reference: AC-158-001 in STORY-158; CI gate introduced in wave-72.)"
exit 1