Skip to content

feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows - #544

Draft
djdomi wants to merge 230 commits into
current_devfrom
dev/ci-rewrite-1.2
Draft

djdomi wants to merge 230 commits into
current_devfrom
dev/ci-rewrite-1.2

Conversation

@djdomi

@djdomi djdomi commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Full rewrite of the distcc-ng CI per #479: one Bash engine, one regression suite, one machine-readable source of truth, and five thin orchestrator workflows. Draft while the remaining items below are open. The step-by-step record, with evidence per commit, is the living status comment: #544 (comment)

Refs #479

Structure

  • .github/scripts/ci.sh: the only CI engine. Fail-closed CI_COMMANDS dispatch, an SOT index built once per process (one awk pass, binary-search lookups), one owner per capability (image build, container run, stacks, downloads, registry, attestation, artifacts, release identity, event values).
  • .github/scripts/ci.bats: the regression suite for every phase and guard, green and red paths via fixtures.
  • .github/yaml/build-manifest.yml: the sole pin and policy owner (image digests as name:tag@sha256, tool versions with mandatory sha256, the two transport-only action pins, build matrix with each variant's configure, cflags, ldflags, ccache, check_env, build_steps and test_steps, impact classes, housekeeping tasks, release inventory).
  • .github/workflows/{validate,security,release,nightly,housekeeping}.yml: orchestration only. run: calls one ci.sh command; the only uses: steps are actions/cache and actions/upload-artifact, pinned in the SOT and fed exclusively by ci.sh step outputs.
  • Removed: every legacy workflow, scripts/, all .github/actions/ composite actions, the e2e orchestrator scripts, docker/verify/selftest-ptrace.sh and .github/labeler.yml; their logic lives in ci.sh, their data in the SOT.

Guards (lint)

  • Orchestrator-only run: steps; uses: only as an exact SOT action pin with steps.*.outputs inputs.
  • Every image/tool/action pin in the SOT; Dockerfiles take pins only via ARG; an unused SOT action fails.
  • LF-only line endings, full-length digests, the What:/Why:/From: comment form (AGENTS.md [AG-CODE-001], From: holds Issue/PR pointers only), actionlint, shellcheck.
  • SOT image pins as name:tag@sha256:<64 hex> and a sha256 beside every tool url (ci_guard_sot_pins).
  • YAML literals that repeat SOT values: crons, workflow_dispatch options (package, task), Dependabot milestones, the validate.yml job gate of every SOT phase, the ClusterFuzzLite FROM (ci_guard_sot_mirrors); Dockerfile paths and SOT refs that repeat ci.sh constants: the /ci bind target, the release /out trees, the CFL $SRC directory, the registry (ci_guard_path_mirrors).
  • A timeout-minutes on every workflow job (ci_guard_job_timeouts) and each CI-ERROR id raised in one place (ci_guard_error_ids).
  • No shellcheck disable= or shellcheck source=/dev/null in any shell source of the repository (AGENTS.md [AG-INT-003]: a silenced warning is itself a violation); the guard reads the banned texts from the SOT list ci_engine.banned_shell_texts. shellcheck -x over every shell source of the CI-owned tree. No output discarded to /dev/null in CI code (scripts, workflows, Dockerfiles of the CI-owned paths; SOT list ci_engine.banned_ci_texts); both lists are checked by ci_guard_banned_texts.

Validation

  • Real CI on this branch: Validate and Security green on 6d5bb78, dcbd4d2, 0202a94, f046f5a, ed87de8, 3376e30, f5cf0e0, 66a9f8a, cd2af70, b072143, 115bdb9 and the current tip 50f724a (the living comment lists every tip, run detail and the red runs with their root cause).
  • Real CI on 6008af2: Validate 37617710562 and Security 37617710631 success. On 8485e4f: Validate 37635061911 and Security 37635062363 success. On 92fbc91: Validate 37639401637 success; its e2e distributed mode ran the self-compile through the new warning gate (ng-ng-plain: 100 COMPILE_OK, ng-ng-pump: 98 COMPILE_OK, no BUILD-WARN line). On 617bfc3: Validate 37640296491 and Security 37640296459 success. On 00d18de: Validate 37644342257 success, Security 37644342303 failure (SARIF upload retry, fixed in 9ca629a). On 9ca629a: Validate 37646260743 and Security 37646260647 success. On c5f36bc: Validate 37649000208 and Security 37649000249 success. On 36be7f2: Validate 37651263407 and Security 37651263488 success. On 9f5c935: Validate 37658638162 and Security 37658638053 success. On the tip ce9f997: Validate 37678404656 and Security 37678404742 success (feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment)).
  • ci.bats survivor pass, SOT read efficiency and output masking 7673109 to 71cb071 (218 -> 120 tests with the checkout test; process counts and equivalence evidence; [AG-CODE-004] confirmations): feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment).
  • Audit findings 150f42f to 7b6294d (walker node kinds, SOT-owned variants and housekeeping tasks, path constants, guards moved from ci.bats into ci.sh lint, guard input ids, one owner each for the asset globs and the pin form, label-pr over 300 files): per-commit CI, the 5eb9389 macOS regression and its fix in 37cde7a, the walker check on paths PR CI does not run (412/412 reads on the real SOT) and the [AG-CODE-004] confirmations are in feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment).
  • Real CI on 60b0f95: Validate 36979646233 and Security 36979646260 success.
  • Real CI on 643b8ec: Validate 37004243842 and Security 37004243750 success; the verify job (110828834639) logs [CI-SELFTEST] actionlint: OK with the SOT-pinned release binary, and every image build stage logs 0 not upgraded after apt-get full-upgrade (validate run 37000169812).
  • Compile cache restored in real CI (0ba323e: 112 ccache hits on ubuntu, 106 on macOS from the previous run's save); coverage-reports and scorecard-results artifacts uploaded; build-provenance attestation stored and verified as SLSA v1 (68fd599).
  • Published ghcr.io/wiki-mod/distcc-ng-buildtools@sha256:d9964924… on the evidence host: ci.sh lint (actionlint, shellcheck, every guard) green on the tree of each code commit d6f5d2f to ebf88c0 before it was pushed, except 680b203, red on the two Alpine initd suppression lines until 7d37820 (as its commit message records); for the docs-only commits cdfadd7 and 60b0f95 the evidence is the real CI lint job (job 110751304473, success). For 4527c34 to 643b8ec and 92fbc91 to ce9f997 the same ci.sh lint ran green on each tree before it was pushed. That image has no bats, so ci.bats (219/219 at ce9f997) was iterated in a throwaway container of it plus apt bats, which is not AG-VAL-003 evidence; the evidence for ci.bats is the real CI engine-selftest job. Each new test was also run against the previous ci.sh (or workflow) and failed there.

Audit closure

The three audits of 50f724a (owner/coverage, return paths, short form) are closed per finding in #544 (comment): status, path and line at the tip, rule, fix commit, green test and red evidence for every ID, evidence limits stated as measured.

Every file this PR deletes (57) is mapped to its new owner, or marked obsolete with the reason, and #479 is checked requirement by requirement in #544 (comment). That audit found and fixed the lost daily full-matrix check of current_dev (1ef8c35), phases without fixture tests (cec4c35, 9091aac, 848dcc5), .trivyignore.yaml outside the comment guard (05c4885), and two apt diagnostics/bounds (cec4c35, ce9f997).

Merge prerequisites (maintainer)

  1. Rulesets. current-dev-protect (20746300) and distcc-ng-default (18300729) still require the legacy contexts (make_check (ubuntu-latest), make_check (macOS-latest), action-lint, require_changelog, shellcheck, scan-pr / osv-scan, PR title Conventional-Commit lint, PR tracking metadata (labels/milestone/project), Test coverage (gcov/lcov, job summary + artifact), Bundled popt fallback build (no system libpopt), Vendored popt/ version and compile check, Distributed compile E2E (2-container)). None of them is emitted after this PR, so every PR would wait on them forever. Both rulesets need, in lockstep:

    Legacy context(s) New required context
    make_check (*), Bundled popt fallback build …, Vendored popt/ …, Distributed compile E2E (2-container), Test coverage …, action-lint, shellcheck Validate (required) (gate over lint, self-test, plan, build/test matrix, e2e, container, package, verify)
    require_changelog, PR title Conventional-Commit lint, PR tracking metadata (labels/milestone/project) PR metadata (title/tracking/changelog)
    Analyze (c-cpp), Analyze (python), Analyze (actions) unchanged names
    scan-pr / osv-scan OSV scan

    Rulesets are maintainer-exclusive (AGENTS.md [AG-SEC-006]); this PR does not change them. CI Rewrite 1.2 #479 asks for the ruleset change to be dry-run first, for example both rulesets in evaluate mode with the new contexts before they are switched to active.

  2. AGENTS.md [AG-CI-001] versus CI Rewrite 1.2 #479's target structure. Resolved in 6008af2 and 8485e4f on the maintainer's ruling: [AG-CI-001] now requires CI logic once in ci.sh, every pin in the SOT and one ci.sh command per run: step, and forbids composite actions under .github/actions/.

  3. Stale file references in AGENTS.md. Resolved in 8040030: [AG-GH-002] now names .github/workflows/validate.yml's metadata job, and [AG-GH-014] names .github/scripts/ci.sh's _ci_check_pr_title, which reads its types and scopes from that rule's own line (e7d7853). Only the file references changed, no requirement. Please confirm the wording when you review.

  4. GitHub Code Quality. CI Rewrite 1.2 #479's Constraints remove both GitHub-managed CodeQL entries (default code-scanning and code quality). Default code-scanning setup is not-configured; CodeQL - Code Quality (dynamic/github-code-quality/codeql) still runs on master (last run 37297303872 on 2026-10-05, checked 2026-10-07). Turning it off is a repository setting. For information only: this PR's codeql job runs security-extended, so Code Quality's quality queries run nowhere else.

  5. Schedules, dispatches and Dependabot read master's workflow copies only, so the new housekeeping/nightly schedules, sot-update and .github/dependabot.yml take effect with the release that promotes this to master.

  6. Open decisions from the 2026-10-02 audit pass. All settled: ci.sh image cfl-toolchain writes CFL's $SRC/build.sh and .clusterfuzzlite/build.sh is gone, every job has timeout-minutes, _ci_fetch_tool fails closed (7f5e2a3); the unreachable fork-PR branch of _ci_check_pr_board is removed and actionlint is a SOT-pinned release binary with no golang builder stage (643b8ec). (b) [AG-INT-006] is removed and its texts are the SOT list (6008af2); the duplicate e2e harness is gone since c339651, one ci.sh e2e harness remains (feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment)). Recorded as pre-existing and outside this rewrite: the trivy --ignore-unfixed and OSV pomxml flags (feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment)).

  7. Milestone on bot pull requests. Resolved in 9f5c935 from [AG-GH-002] and the milestone the repository's own milestoned pull requests carry: bot pull requests carry the SOT milestone bot_milestone ("current_dev backlog"), sot-update adds its pull request to the board, and dependabot.yml's milestone is bound to the SOT.

  8. Open decisions from the 2026-10-08 audit pass (CodeQL --download, popt-vendor popt-strict, the release CHANGELOG push to current_dev): feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment). Also open: /dev/null in the product shell files autogen.sh, Makefile.in and pump.in (feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment)).

Not exercised by this PR's CI

report, the OpenSSF recheck, sot-update, nightly (including the harden-runner agent lifecycle and e2e full) and the release jobs run only on schedules, tag pushes or dispatches from master. The ClusterFuzzLite crash upload has never fired, because no fuzz run crashed. The variables job (label-pr, add-to-project) runs on pull_request_target and issues, which use the default branch's workflow file, so it has not run in its new form either; that includes the label-pr fix in 7d62fba. ci.bats covers their decision paths.

Upstream relevance (AGENTS.md [AG-UP-001])

Everything else here is this fork's own CI. Non-CI fixes touch packaging/RedHat/rpm.spec and the Alpine OpenRC units:

  • 655908b: a prose comment that made rpm expand a second setup macro came from this fork's own commit 9990404; upstream distcc/distcc's packaging/RedHat/rpm.spec has no such comment (checked 2026-10-01), so no entry applies.
  • 97710ef and 00335ea: two commented-out directives that rpmbuild still expanded ("Macro expanded in comment"), the unknown configure option --with-docdir and the absolute masquerade symlinks all come from upstream's own spec at 8d569d19 (checked 2026-10-01); the comment macros are also visible in make deb fails distcc/distcc#418's log. 8783920 and 16f32a5 add support-upstream/issue-479-rpm-spec-build-warnings.md and its index row 60.
  • 6008af2 to 617bfc3 change only this fork's CI engine, suite, SOT and governance text, so no upstream entry applies. The same holds for 150f42f to 7b6294d (ci.sh, ci.bats, the SOT, release.yml, the release Dockerfile comment and the CI docs).
  • 7d37820: the two packaging/Alpine*/distccd.initd OpenRC units are this fork's own; upstream distcc/distcc has no packaging/Alpine (checked at 8d569d19, 2026-10-02), so no entry applies.

Review state

0 unresolved review threads (checked 2026-10-08). Self-audit and VER-* classification for 617bfc3 are in the living comment; real CI of the tip ce9f997 and its self-audit (34 files; functions 338/338, tests 219/219, comment form 17/18 for rpm.spec's upstream directives, LF 34/34), and the independent reviews of 9ca629a, c5f36bc, 36be7f2 and 9f5c935 with their fixes, are in #544 (comment). Not claimed ready: merge prerequisites 1 and 4 are maintainer settings.

@github-actions github-actions Bot added enhancement New feature or request ci Continuous integration and runner workflow changes and removed enhancement New feature or request labels Sep 15, 2026
@djdomi djdomi added this to distcc-ng Sep 15, 2026
@djdomi djdomi added the no-changelog-needed Opts a PR out of the CHANGELOG.md-required CI check label Sep 15, 2026
@djdomi djdomi added this to the current_dev backlog milestone Sep 15, 2026
@github-actions github-actions Bot added the enhancement New feature or request label Sep 15, 2026
Phase 0 of the CI Rewrite 1.2 (Refs #479): introduce the single CI
engine and its inputs as the foundation for the orchestrator model.

- .github/yaml/build-manifest.yml: single source of truth (base image
  digests, external tool versions, build matrix, release inventory,
  impact classes); full-length SHAs only.
- .github/scripts/ci.sh: fail-closed CI_COMMANDS dispatch, awk-only SOT
  readers, nproc*2 (min 16) bats job count, resolve, and the
  content-based impact phase (DEFAULT=NOOP; a docs-only diff selects
  doc-lint, never a compile). Remaining phases fail closed.
- .github/scripts/ci.bats: regression suite for dispatch, SOT readers,
  parallelism floor, resolve, and impact classification.

WIP: legacy CI untouched; no workflows rewired yet.
Phase 0 (Refs #479): first two governance guards, run by the `lint`
phase and covered green+red by ci.bats fixtures.

- ci_guard_line_endings: fails closed on any CR byte (LF-only repo).
- ci_guard_full_sha: fails closed on any sha256 digest that is not full
  64-hex, or any `uses:` action pin that is not a full 40-hex SHA.
- ci_cmd_lint wires both; full-SHA scans only pin-bearing trees
  (.github/workflows|actions|yaml, docker), not .github/scripts, since
  ci.sh carries no pins by design and ci.bats holds deliberate fixtures.

Real-tree lint is clean; both guards proven on green and red fixtures.
Phase 0 (Refs #479): bind Dependabot's Dockerfile digest bumps to the
SOT so the two cannot drift.

- ci_guard_dependabot_consistency: fails closed if a base_images pin in
  build-manifest.yml is not present verbatim in its Dockerfile
  (debian_verify/golang_actionlint -> docker/verify/Dockerfile,
  debian_release -> docker/release/Dockerfile). A Dependabot bump that
  does not reach the SOT turns the lint phase red.
- Wired into ci_cmd_lint; real-tree lint clean, guard proven green+red
  on self-contained SOT+Dockerfile fixtures.
Phase 0 (Refs #479): forbid inline logic in workflow run: blocks; a
run: step calls one command (bash ci.sh <phase>) only.

- _ci_scan_run_blocks / ci_guard_orchestrator_only: flag control-flow
  keywords, &&/||/;, pipes, command/backtick substitution, heredocs,
  bash -c / python -c, awk/sed/jq, and set -e/-u/-o inside run: blocks.
- ci_cmd_lint scans every .github/workflows/*.yml; there is no legacy
  exemption -- every shipped workflow is an orchestrator.
- Proven green+red on fixtures. Against the current tree it reports 285
  inline-logic violations across 12 not-yet-rewritten legacy workflows;
  these clear as each workflow is rewritten and removed.
Phase 1 (Refs #479): fold c-build.yml's per-variant build commands into
ci.sh so a workflow runs `ci.sh build <variant>` only. NOT yet verified
in the buildtools container (real build/test runs are container-only,
rule 87); this commit is the faithful command fold.

- ci_cmd_build: default (ccache CC + python3.13||python3), popt-fallback
  (asserts the bundled-popt fallback in configure.log), popt-vendor
  (--without-system-popt + standalone Werror gcc compile of popt/*.c),
  coverage (--coverage -O0 --with-seccomp), sanitizer (ASan/UBSan flags,
  --without-seccomp). Unknown variant fails closed before autogen.
- _ci_has_compiler_warning: warnings-are-errors (rule 31), anchored to
  the gcc/clang diagnostic line shape (catches the non-Werror
  include-server setup.py path).
- _ci_popt_strict_compile: the vendored-popt strict compile.
- ci.bats: unknown-variant rejection (green build path is container-only).
Phase 1 (Refs #479): fold run-tests.sh's comfychair verdict and
c-build.yml's per-variant make-check env into ci.sh. Real make-check
runs are container-only (rule 87); this is the faithful fold.

- _ci_parse_comfychair: OK/NOTRUN/FAIL parse; 0/0/0 parsed is a hard
  fail (rule 66); any FAIL fails.
- ci_cmd_test: default/popt-fallback (plain), sanitizer (ASAN/UBSan
  options), coverage (coverage-python-wrapper + lcov capture),
  popt-vendor (no make-check phase). Warnings-are-errors on the check
  log; make-check exit code honored; unknown variant fails closed.
- _ci_privileged_single_test (root-only AutogroupNiceness case),
  _ci_coverage_python_wrapper, _ci_coverage_lcov helpers.
- ci.bats: parser green/FAIL/zero + test unknown-variant.
Phase 1 (Refs #479): wire the content-based matrix and self-test.

- ci_cmd_matrix: emit the build matrix JSON (variant x os) from the SOT,
  excluding opt-in variants (sanitizer).
- ci_cmd_plan: write phases/build/matrix to $GITHUB_OUTPUT for the
  base..head diff, so the orchestrator needs no inline logic.
- ci_cmd_selftest: run ci.bats in parallel (--jobs max(16,nproc*2)).
- New impact class ci-engine (.github/scripts/**) -> selftest+lint, so a
  change to ci.sh/ci.bats runs the regression suite instead of NOOP.
- impact/plan now cd into the repo root (consistent with build/test)
  rather than git -C.
- ci.bats: matrix contents (default on both OSes, no sanitizer).
Phase 1 (Refs #479): first thin orchestrator workflow. Runs on
pull_request/push and executes the ci.bats regression suite via
`ci.sh selftest`, so the engine is verified in real CI. Runs alongside
the existing legacy CI (which still provides the required checks); no
legacy workflow removed yet.

Orchestrator-clean (AG-CI-023): every run: is a single command; checkout
pinned to a full 40-hex SHA. Passes the orchestrator/full-SHA/
line-endings guards.
The run-block scanner only matched `run:` at line start, missing the
`- run: |` list-item form, so inline logic in that form went undetected
(caught by ci.bats test 27 failing in real CI). Match an optional `- `
prefix. Real-tree violations rise 285 -> 288 as the missed forms are
now seen.
The build matrix job needs each variant's dependency list. Add apt
(ubuntu) and brew (macOS default) to build_matrix.variants in the SOT,
folded from c-build.yml's per-job install-build-deps/brew inputs, and
have ci_cmd_matrix emit apt for linux entries and brew for macOS
entries. One owner for the dependency lists; the workflow reads them.
The plan job runs ci.sh plan (content-based impact) and outputs the
build flag + matrix; build_test consumes the matrix (fail-fast off),
installs per-variant deps (install-build-deps apt on Linux, brew on
macOS), and runs ci.sh build/test per variant. ci_cmd_plan now falls
back to a full run when the base commit is unknown (branch creation /
workflow_dispatch). workflow_dispatch added so the matrix can be
exercised without a c-source change.
- ci_cmd_plan used ${1:?...} which aborts on an empty base (the
  workflow_dispatch case), before the unknown-base fallback could run;
  accept an empty base and route it into the full-run fallback.
- The matrix self-test still expected the pre-apt/brew entry shape;
  loosen it to match the current per-entry format.
_ci_privileged_single_test ran the AutogroupNicenessPrivilegeDrop_Case
rerun on macOS too, where autogroups are a Linux-only kernel feature, so
it reported NOTRUN and the guard failed the default/macOS build_test job
(make check itself was clean: OK=171 NOTRUN=43 FAILED=0). Guard the
rerun to Linux, matching c-build.yml's matrix.os == 'ubuntu-latest'.
actionlint/shellcheck flagged $BREW in 'brew install $BREW'; the
word-split is intentional (a package list from the SOT). Add a scoped
shellcheck disable directive as the first line of the run script.
ci_cmd_e2e wraps the existing 2-container distributed-compile harness
(test/e2e/run-e2e.sh); the distributed-e2e-test composite action folds
away. validate.yml gains an e2e job gated on the plan phases output.
Folds check-pr-title-convention.sh into ci.sh as _ci_check_pr_title
(rule-71 taxonomy: allowed types/scopes, dependabot exemption,
draft/warn/block modes). ci_cmd_metadata dispatches the PR-context
checks; tracking-metadata and changelog gate follow. Part of replacing
changelog-check.yml (removes the #479-SectionA cancel-in-progress race).
Folds check-pr-tracking-metadata.sh's enforced core (labels + milestone,
rule 3; project-board full check best-effort as in the legacy degraded
mode) and require_changelog (CHANGELOG.md touched or no-changelog-needed
label). ci_cmd_metadata all runs title+tracking+changelog -- the
replacement for changelog-check.yml's three PR-context checks.
- ci_cmd_package folds build-release-packages.sh (autogen, configure
  --enable-Werror, make deb) with a fail-closed missing-tool check.
- _ci_check_release_version folds check-release-version.sh (configure.ac
  AC_INIT must equal the tag; tag must not already exist). ci_cmd_release
  version-check runs it; the actual tag/publish is a maintainer-gated
  outward action, not folded as an autonomous step.
Consolidated security scans as a workflow_dispatch-only cutover-draft
(runs only when the cutover flips triggers and removes the legacy scan
workflows; running both would double-upload SARIF). CodeQL c-cpp builds
via ci.sh build default; ci_cmd_build now makes ccache optional so the
no-ccache CodeQL apt set still builds. Pins folded verbatim from the
legacy scan workflows. Full verification happens at cutover.
ci_cmd_container (nightly: docker build+push with SOT base ARG),
ci_cmd_publish/_ci_publish_nightly (force-move the floating nightly tag
with a v* guard, (re)publish the prerelease with built assets), and
ci_cmd_e2e full (bidirectional matrix). nightly.yml is a
workflow_dispatch-only cutover-draft (build/test/e2e via ci.sh + package
+ container + publish); the schedule trigger + legacy removal come at
cutover. Fail-closed paths (unknown variant, v* tag guard) covered.
ci_cmd_gc is a verbatim fold of ghcr-cleanup.sh (untagged + old manual-N
GHCR versions pruned, live multi-arch children protected, real/latest/
nightly tags never touched; DRY_RUN=true default). housekeeping.yml is a
workflow_dispatch-only cutover-draft wiring it with package/dry_run
inputs. Scheduled heartbeat + OpenSSF-recheck fold at cutover.
ci_cmd_container now builds plain/pump per platform (runtime/runtime-pump
targets, SOT base ARG) and records the pushed digest; ci_cmd_publish gains
manifest (buildx imagetools create from per-platform digests, amd64
required/arm64 best-effort, latest on real tag) and github-release
(version-check gated + gh release with built assets). release.yml is a
workflow_dispatch-only DRAFT (setup/build/test/e2e/package/container-
matrix/manifest/github-release). DRAFT: Trivy/SBOM and run_attempt digest
keying fold at cutover; needs real-tag verification before it goes live.
djdomi added 11 commits October 8, 2026 07:54
- Unknown commands, variants, modes and subcommands: seven tests
  become one. The real entry keeps one row (bash ci.sh bogus-command);
  the other ten rows call ci_main in-process with docker, make and
  configure forbidden, so "before any work" holds for every row.
- sot set absorbs its rename/mode test; the duplicate missing-path
  case goes.
- PR titles: one table on a fixture AG-GH-14 rule covers valid,
  no type, unknown type, unknown scope and empty titles in block,
  warn and draft (warn, draft, type and scope were untested before;
  mutants dropping the draft exemption or the scope check now fail).
  The taxonomy test parses a fixture rule instead of copying the
  real lists from AGENTS.md.
- Tracking: one table (both set, no milestone, whitespace-only label);
  the label branch was untested before and its mutant now fails.
- Board check: one table (no PAT warns; on, off, lookup error with a
  PAT).
- Release version-check: one fixture tag repo for all rows instead of
  one per test; the configure.ac mismatch row moves off the real repo.
- Release context: one table per event (tag push, dispatch with and
  without opt-in, no tag, branch push, schedule).
- Changelog event: one table (release and notes insert; pre-release
  and empty notes skip without an insert).
- Registry: login and push without a token, then the stdin login, in
  one test.

Mutants: dropping the label check, the pre-release skip or the
existing-tag check each fail their table row.
- e2e compile-ok counter and server warning scan: each red case joins
  its green test.
- changelog check: the opt-out label, zero-SHA and failed-diff rows
  join the merge-base test.
- comfychair: one table for OK+NOTRUN, FAIL, no result line and a
  missing log (the last moved out of the mixed changelog/comfychair
  test); a mutant without the log check fails its row.
impact-hit, matrix, make gate/configure, report, output writer,
artifact offer, cache plan, CFL run, draft release, OpenSSF checks and
verdicts, OpenSSF recheck, gc protection and the run gate each keep
one test; the red or second case runs in the same test. Shared state
is reset between the parts (output files, the CFL workspace), the
impact-hit broken-diff row runs before the path stubs it needs to
bypass, and the OpenSSF advisory file no longer reuses a directory
name.
Survivor pass with the bidirectional fault-separation rule: two tests
stay separate only if one realistic defect fails A but not B and
another fails B but not A. Tests that shared one final owner and one
observable contract now run as one test each:

impact path classes (5), plan (3), classify (3), glob (3), image
build (3), container run (3), tool fetch (3), harden stop (3),
tracking (2), changelog update (2), add-to-project (2), route (2),
wait-until (2), download (2), sot refresh (2), SARIF upload (2),
sot-update (2), harden start (2), and per lint guard: line endings,
full SHA, unreadable or empty input, pins, comments, orchestrator,
shellcheck directives.

Parts that need the real function run before the part that stubs it
(changelog retry, plan dispatch refs), and each part resets the files
it asserts on (argv, tries, output, CI_MANIFEST, the git stub).
pr category (3 tests, one owner), tool fetch and its stuck partial
dir, harden stop and its unreadable state, the OSV PR gate (2), the
OpenSSF checks and the BR-01 scope test, and apt install with the
image full-upgrade test each become one test. The harden stop parts
reset the agent dir between them.
Every _ci_sot_* read ran awk over the whole build-manifest.yml; one
ci.sh plan read the file 40 times. ci_require_manifest now indexes
the SOT once in the parent shell (one awk pass, a C-locale sort, one
eval of two arrays), so every subshell inherits the index, and a
lookup is a binary search in bash with no fork.

- Same rules as the old walker: first hit per path, children only
  from a section's first occurrence, the same value and comment
  parsing. 420 reads (every call site expanded against the real SOT,
  plus error controls) are byte-identical old vs new under bash 5
  and bash 3.2 (busybox awk).
- Validity: the index is keyed by the CI_MANIFEST path and dropped
  by _ci_sot_set; a caller that writes in a subshell drops its own
  index (ci_cmd_sot_update after _ci_sot_refresh).
- New fail-closed ids: SOT-0011 odd indentation (the walker read it
  silently wrong), SOT-0012 unreadable SOT, SOT-0013 set on a section.
  The writer keeps its own awk rewrite (_ci_sot_write).
- Values with ', $ and backticks round-trip exactly (reader table
  row); a mutant without the quote escaping fails loudly in eval, a
  mutant without the drop in _ci_sot_set fails the sot set test.
- ci.bats indexes the real SOT once per file (setup_file) and each
  fixture once (_fixture_manifest): one shell variable per path made
  bats 40 ms slower per test, two arrays do not.

Process counts (shimmed, buildtools image): ci.sh plan awk 56 -> 18
(SOT reads 40 -> 1); ci.bats awk 1305 -> 583 (SOT reads 896 -> 91).
- _ci_sot_list split each list with tr | sed | awk; it now splits
  in bash with the same rule (trim blanks, then one quote, at each
  end; drop empty items).
- ci_cmd_matrix and _ci_release_matrix ran one jq per variant x os
  or variant x platform, then one more to join; one jq now builds
  each matrix from the collected rows.

Output is byte-identical old vs new: 420 SOT reads under bash 5 and
bash 3.2, and both matrices on the real SOT and on a fixture with
backslashes, quotes, spaces and a bad optional flag.
Process counts for ci.sh plan (dispatch), 9541430 -> this commit:
awk 18 -> 3, jq 13 -> 4, sed 15 -> 0. ci.bats: awk 583 -> 284,
sed 568 -> 269, jq 258 -> 233.
_ci_check_pr_title read AGENTS.md twice (types, scopes) and ran a
grep|tr|tr pipeline per list plus two sed trims. _ci_title_rule now
reads the rule line once and _ci_title_taxonomy parses a given line
with a bash regex; the trims are parameter expansions. Called without
a line, _ci_title_taxonomy still reads the rule itself.

Per title check: AGENTS.md reads 2 -> 1, processes 9 -> 1 (one grep).
Taxonomy lists, error ids and every title verdict (valid, trailing
blanks, spaced subject, unknown scope/type, no subject, breaking,
untyped, empty; block and warn) are identical old vs new on the real
AGENTS.md.
label-pr (PR number, changed_files), the dispatch release context
(tag, publish opt-in) and the release changelog event (prerelease,
tag_name) ran one jq per field over the same event file. Each now
reads its fields in one _ci_event_value call with a comma filter,
the form _ci_event_range already uses. The multi-line release body
stays a separate read.

One stricter case: a release event with prerelease true but no
tag_name used to be skipped without reading the tag; it now fails
with EVENT-0001 like every other malformed payload. A GitHub release
event always carries tag_name.
jq per operation: label-pr 2 -> 1, release context 2 -> 1, changelog
release 3 -> 2; ci.bats jq 233 -> 224. Lint 0, bats 119/119.
AGENTS.md [AG-INT-003] forbids hiding real output. ci.sh discarded
command output at 24 places (docker rm/create/run ids, ccache, jq -e,
command -v, SOT existence reads, sudo tee) and two command -v calls
also discarded stderr; git rev-parse --quiet hid its error; the ssh
self-test sent its host key to /dev/null.

- Each discard now goes to stderr, where it stays visible in the log
  without polluting a function's stdout return value.
- git rev-parse --verify drops --quiet; the ssh self-test keeps
  known_hosts in its own temp dir.
- ci_main and the dispatch test read declare -F as a value instead
  of printing a function name on every dispatch.
- ci.bats separates stderr (run --separate-stderr, bats >= 1.5) where
  a test compares a function's exact stdout, and its sudo stub writes
  to a file instead of /dev/null.

Recurrence: ci_guard_shellcheck_directives becomes
ci_guard_banned_texts (same ids) and also applies the new SOT list
ci_engine.banned_ci_texts (/dev/null) to CI code in the CI-owned
paths: scripts, workflows and Dockerfiles (CI-ERROR-GUARD-SHELLCHECK-
0006). The SOT itself is data and holds the banned texts. Product
shell files are not in this list's scope.
Mutants: dropping the CI-text check or its owned-path scope each
fail the guard test. Lint 0, bats 119/119.
@djdomi

djdomi commented Oct 8, 2026 •

Copy link
Copy Markdown
Member Author

Update: 7673109..71cb071 (ci.bats survivor pass, ci.sh read efficiency, no output masking)

Follows #544 (comment).

Commits and real CI

Commit Change Validate / Security
7673109, 3307fa2, 4e942a9 ci.bats tables: job count, SOT readers, unknown input, sot set, PR titles; three tests with no unique claim deleted cancelled by the next push; covered by 9c58d7c
9c58d7c tables for tracking, board, version-check, release context, changelog event, registry success 37735127246 / 37735127212
673eca3, f86d58b red cases absorbed into their green tests (18 groups) success 37736245367 / 37736245294
588b62c, 986f5b0 one test per owner where survivors shared one owner and contract (31 merges) success 37737339277 / 37737339394
9541430 the SOT is read once per process (index + binary search) instead of one awk per lookup success 37742411550 / 37742411572
826c438 SOT lists and both matrices without a process per item success 37743490723 / 37743490584
55bcfb7, 236d65c the AG-GH-14 rule read once per title check; one-line event fields read in one jq run success 37744282002 / 37744282027
71cb071 no output discarded to /dev/null in CI code, with a guard against recurrence success 37746075952 / 37746075935

Pushing every step cancelled the previous run (validate/security concurrency); from 9c58d7c on, the next push waited for the previous run to finish.

ci.bats: 218 -> 119 tests

The survival pass now applies this separation rule: two tests stay separate only with a concrete defect that fails one and not the other, in both directions; tests sharing one final owner and observable contract are rows of one test. 99 declarations were merged or deleted; every merged group kept all its assertions, and each table row names its case on failure. Mutants were run on the changed owners (job-count floor, SOT node kinds, unknown-input rejection, title draft/scope branches, label check, pre-release skip, existing-tag check, comfychair log check, tarball filter); each fails its row. The pass continues: separation-by-table for survivors with distinct owners but one call path, the checkout phase (no test yet), and the split of the popt test.

SOT read path (P1, 9541430)

Every _ci_sot_* read ran awk over the whole build-manifest.yml; one ci.sh plan read it 40 times. ci_require_manifest now indexes the SOT once in the parent shell (one awk pass, a C-locale sort, one eval of two arrays); every subshell inherits the index and a lookup is a binary search in bash.

  • Equivalence: 420 reads (all 136 call sites expanded through their callers against the real SOT, plus error controls) byte-identical old vs new under bash 5 and bash 3.2; the macOS leg of 9541430 built and tested through the new index (OK=171 NOTRUN=43 FAILED=0).
  • Validity: keyed by the CI_MANIFEST path, dropped by _ci_sot_set; a caller that writes in a subshell drops its own index.
  • New fail-closed ids: CI-ERROR-SOT-0011 odd indentation (the old walker read it silently wrong), CI-ERROR-SOT-0012 unreadable SOT, CI-ERROR-SOT-0013 set on a section.
  • Two rejected designs, measured: a pattern search on one string (plan 165 -> 3746 ms) and one shell variable per path (bats +38 ms per test).

Process counts for ci.sh plan (dispatch), before 9541430 -> after 236d65c: awk 56 -> 3, jq 13 -> 4, sed 15 -> 0. ci.bats: awk 1305 -> 284, SOT reads 896 -> 91. Real-CI ci.sh selftest step: 11 s at 986f5b0, 10 s at 9541430.

No output masking (71cb071)

AGENTS.md [AG-INT-003] forbids hiding output. ci.sh discarded output at 24 places (docker ids, ccache, jq -e, command -v, SOT existence reads, sudo tee), two of them also stderr; git rev-parse --quiet hid its error; the ssh self-test sent its host key to /dev/null. Each discard now goes to stderr; predicates read their value. ci.bats separates stderr (run --separate-stderr) where a test compares a function's exact stdout.

Recurrence: ci_guard_shellcheck_directives became ci_guard_banned_texts (same ids) and also applies the new SOT list ci_engine.banned_ci_texts (/dev/null) to CI code in the CI-owned paths: scripts, workflows and Dockerfiles (CI-ERROR-GUARD-SHELLCHECK-0006). Product shell files (autogen.sh, Makefile.in, pump.in) are outside this list's scope; that is an open maintainer decision.

Decisions needed

  • /dev/null in product shell files (autogen.sh, Makefile.in, pump.in): separate issue/PR to remove it and extend the ban repo-wide, or keep upstream behaviour there.

[AG-CODE-004]

Each search ran on the parent of the introducing commit, over every CI file tracked there (git ls-tree of .github, docker, .clusterfuzzlite, test/e2e: 14 files).

  • I confirm I did not violate [AG-CODE-004] -- I searched 9541430~1 for an SOT cache or index (_CI_SOT, index, eval, declare -A) and found none, only the per-lookup awk walker _ci_sot_lookup with its built-in set mode, so I am implementing _ci_sot_index and _ci_sot_index_drop and splitting the set mode into _ci_sot_write in .github/scripts/ci.sh.
  • I confirm I did not violate [AG-CODE-004] -- I searched 9541430~1 for a once-per-file bats setup (setup_file, BATS_FILE_TMPDIR) and found none, so I am implementing setup_file in .github/scripts/ci.bats to index the read-only SOT once per file.
  • I confirm I did not violate [AG-CODE-004] -- I searched 826c438~1 for list splitting in bash and found the read -ra form in seven places of .github/scripts/ci.sh, so I am modifying _ci_sot_list to use that form instead of tr | sed | awk.
  • I confirm I did not violate [AG-CODE-004] -- I searched 826c438~1 for matrix JSON builders and found one jq -cn per row plus a join in ci_cmd_matrix and _ci_release_matrix, so I am modifying both in .github/scripts/ci.sh to build each matrix in one jq run.
  • I confirm I did not violate [AG-CODE-004] -- I searched 55bcfb7~1 for readers of the AG-Unbounded strcat command-line construction overflows 261-byte buffer in dcc_execvp_cyg (Cygwin) #14 rule and found only _ci_title_taxonomy, called once per list, so I am implementing _ci_title_rule and modifying _ci_title_taxonomy and _ci_check_pr_title in .github/scripts/ci.sh to read the rule once.
  • I confirm I did not violate [AG-CODE-004] -- I searched 236d65c~1 for multi-field event reads and found the comma-filter form in _ci_event_range, so I am modifying _ci_variables_label_pr, _ci_release_context and _ci_changelog_from_event in .github/scripts/ci.sh to use that form.
  • I confirm I did not violate [AG-CODE-004] -- I searched 71cb071~1 for banned-text checks (banned_shell_texts, _ci_banned) and found ci_guard_shellcheck_directives with _ci_banned_shell_texts, so I am extending that guard as ci_guard_banned_texts with _ci_banned_texts and _ci_banned_hits in .github/scripts/ci.sh and the SOT list ci_engine.banned_ci_texts in .github/yaml/build-manifest.yml.

Correction to #544 (comment): its parent searches ran over a hard-coded file list that named a Dockerfile that never existed and left out ci.bats and test/e2e. Rerun over the tree-derived list, one bullet changes: at afe08f9~1 the name:tag@sha256 form was also checked by a regex in a ci.bats test, besides _ci_sot_refresh. afe08f9 replaced that test with the ci_guard_sot_pins test, and 7b6294d made the two remaining ci.sh rules share _ci_pin_tracked. The other bullets are unchanged.


Updated 2026-10-08 12:37 CEST: two statements above are no longer current, see #544 (comment). Tests merged by appending bodies named their failing part only from 5cefa29 on; before, such a failure showed a bare [ ... ] line. The checkout phase test landed in 02f2042, and the popt test was rewritten in place in 3c41a3f instead of being split.

djdomi added 4 commits October 8, 2026 10:06
ci_cmd_checkout had no test; real CI runs only its green path. One
table against a file:// fixture server: depth 1 is shallow at the
requested commit, depth 0 holds the full history, an unknown ref
fails without a checkout, and a missing GITHUB_REPOSITORY stops the
command. A mutant that ignores the depth fails the shallow row.
120 tests; every CI_COMMANDS phase is now reached by a test.
Validate 37747652756 failed on 02f2042: the GitHub runner always sets
GITHUB_REPOSITORY, the test inherited it, so the "missing env" row
fetched instead of stopping (fatal: Needed a single revision). The
evidence host had no such variable, so the row passed there.
Reproduced with GITHUB_REPOSITORY set; the row now removes it with
env -u, as the other tests do with the runner variables they need
absent. The other env-dependent tests already unset theirs.
The appended-body merges (f86d58b, 588b62c, 986f5b0) kept all
assertions, but a failure showed only a bare [ ... ] line. Each part
now starts with echo "case: <original test name>", so the last case
line in a failing test's output names the failing part. Placement was
derived from the pre-merge files and checked mechanically: all 107
markers start at a line of their original test or at a state reset.
The rows added in 71cb071 to the banned-text test get their own case.
The strict-compile half of the popt test ran gcc stubbed to pass, so
it proved the loop but not what the loop passes to gcc. gcc is now
the existing _fake_tool on PATH: the test asserts exactly five calls,
each with the exact flag run -Isrc -Ipopt -Wall -Wextra -Werror
-Wno-unused -Wno-unused-parameter for its popt/*.c file, and that a
failing gcc stops after one call with rc 1 and its error shown.
Four mutants of _ci_popt_strict_compile are each killed at their own
assertion: -Werror dropped, -Wno-error added, no stop on error, one
file dropped. Whether the real popt sources build Werror-clean stays
proven by the real popt-vendor CI leg.
@djdomi

djdomi commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Update: 02f2042..3c41a3f (checkout test, case markers, popt test)

Follows #544 (comment).

Commits and real CI

Commit Change Validate / Security
02f2042 checkout phase test: shallow and full fetch through a file:// remote, a bad ref, missing env Validate failure 37747652756 (below)
e0dbf15 the missing-env row of that test drops GITHUB_REPOSITORY success 37760205992 / 37760206078
5cefa29 each part of a merged test names its case success 37761172395 / 37761172315
3c41a3f popt strict compile pins its flags, file set and stop success 37763277138 / 37763277307

Red run on 02f2042

Validate 37747652756 failed in ci.sh selftest (and therefore gate): the GitHub runner always sets GITHUB_REPOSITORY, the missing-env row inherited it, and checkout fetched instead of stopping (fatal: Needed a single revision). The local runs had no such variable, so the row passed there. Reproduced with the variable set; e0dbf15 starts that row with env -u GITHUB_REPOSITORY. Local iteration runs of ci.bats now set the runner variables (CI, GITHUB_ACTIONS, GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_SHA), so this class shows before CI.

ci.bats: 119 -> 120 tests

  • checkout was the one CI entry point without a test. Reach is now 39/39 entry points (28 phases, 11 guards), counted from CI_COMMANDS and the lint guards, with the bash ci.sh <cmd> entry mapped to ci_main and its ci_cmd_*.
  • Case markers (5cefa29): in tests merged by appending bodies, each part starts with echo "case: <original test name>". bats 1.11.1 prints a test's own stdout only when the test fails (writing-tests.md, "Printing to the terminal"), so the last case line names the failing part. 107 markers; each was checked to start at a line of its original test or at a state reset. A mutant on the artifact offer's empty-set check fails with the marker of that part.
  • popt test (3c41a3f): the strict-compile half ran gcc stubbed to pass, so it proved the loop, not what reaches gcc. gcc is now _fake_tool on PATH; the test asserts exactly five calls, each with the exact flag run -Isrc -Ipopt -Wall -Wextra -Werror -Wno-unused -Wno-unused-parameter for its popt/*.c file, and that a failing gcc stops after one call with rc 1 and its error shown. Four mutants of _ci_popt_strict_compile fail at their own assertion: -Werror dropped, -Wno-error added, no stop on error, one file dropped. Whether the real popt sources build Werror-clean stays proven by the real popt-vendor leg.
  • Earlier mutants rerun on the merged suite (raw logs kept): the guard input check (no-op helper, dropped call), the pin form, the release asset hits and the tarball filter are still killed by the same tests as before the merges.

Corrections to #544 (comment)

  • "each table row names its case on failure" did not hold for tests merged by appending bodies: a failure there showed only a bare [ ... ] line until 5cefa29 added the case markers.
  • "The pass continues: ... the checkout phase (no test yet), and the split of the popt test": checkout is covered since 02f2042. The popt test was rewritten in place, not split: both halves keep their own case marker and assertions, and no required property needs a separate test.

[AG-CODE-004]

Each search ran on the parent of the introducing commit, over the 14 CI files tracked there (git ls-tree of .github, docker, .clusterfuzzlite, test/e2e).

  • I confirm I did not violate [AG-CODE-004] -- I searched 02f2042~1 for a checkout test or a fixture remote (checkout, file://, clone --bare, _fixture_tag_repo) and found no test of ci_cmd_checkout, only the tagged fixture repo _fixture_tag_repo, so I am implementing the checkout test in .github/scripts/ci.bats on that fixture.
  • I confirm I did not violate [AG-CODE-004] -- I searched e0dbf15~1 for how tests drop a runner variable (env -u GITHUB_, unset GITHUB_) and found unset in three tests that call functions in the test shell; the checkout row already starts ci.sh through env, so I am modifying that row in .github/scripts/ci.bats to use env -u GITHUB_REPOSITORY.
  • I confirm I did not violate [AG-CODE-004] -- I searched 5cefa29~1 for a way a merged test names its failing part (echo "case:, BATS_TEST_DESCRIPTION, bats_test_function) and found none, so I am implementing echo "case: ..." lines in .github/scripts/ci.bats.
  • I confirm I did not violate [AG-CODE-004] -- I searched 3c41a3f~1 for a stub that logs its arguments and takes an exit code (_fake_tool, _record, _fail, _stubbed, .args) and found _fake_tool, so I am modifying the popt test in .github/scripts/ci.bats to use it for gcc.

djdomi added 3 commits October 8, 2026 12:38
The lint entry test proved the wiring of one guard only (the banned
text guard ran for real, six others were stubbed to pass). A second
case now records all 13 lint steps: with none failing, the call set
must equal the exact wiring (line endings per owned path, the full-SHA
scan on .github/workflows, .github/yaml and docker, root guards on the
root, workflow guards on the workflow files, error ids on ci.sh, both
linters); then each step fails once, lint must return 1, and every
other step must still have run. Five wiring mutants are each killed at
their own assertion: a dropped guard call, a swallowed failure in the
middle and at the end, a stop after the first failure, and a full-SHA
dir dropped.
…ailed step

The release image test grepped two of the six build calls (install,
pump rename) and the runtime useradd, and checked the stop only for a
failing make and a failing apt install. It now asserts the exact
ordered calls of both image steps: the SOT apt packages in image mode,
configure with --enable-Werror and --without-system-popt (the vendored
popt carries the CVE fixes), the gated make, the install into /out,
make install into /out-pump, the pump rename, and the nologin system
user. Each step then fails once: the function returns 1 and no later
step ran; a failing nproc stops with 2 before make. Five mutants are
each killed at their own assertion: configure not stopping, system
popt, apt runner mode, a swallowed make install failure, a login shell
for the distcc user.
…nd ref

The plan test compared ci_cmd_plan's phases with _ci_all_phases, the
function plan itself calls, so a defect there changed both sides. A
fixture SOT with three impact classes now gives the literal phase set
build package verify. One table covers dispatch and schedule on
current_dev, a dispatch on master (not tested before), bot/x and
544/merge; each row checks phases, build, matrix and
publish_buildtools together. A new case checks that a SOT without the
verify and build phases never publishes buildtools and never builds a
matrix. Six mutants are each killed at their own row: master not
protected, every ref protected, publish without verify, build without
a matrix, a matrix without build, an empty phase set.
@djdomi

djdomi commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Update: f643e31..7db73ae (lint wiring, release images, plan)

Follows #544 (comment).

Commits and real CI

Commit Change Validate / Security
f643e31 lint runs every guard on its input and fails if any fails success 37764800695 / 37764800934
5e1cd37 release images use the SOT packages and stop at the first failed step pushed together with 7db73ae, covered by its run
7db73ae plan expects literal phases from a fixture SOT, per event and ref success 37766771070 / 37766770988

ci.bats (120 tests, three rewritten)

  • Lint wiring (f643e31): the lint entry test proved one guard end to end. A second case records all 13 lint steps: with none failing, the call set must equal the exact wiring (line endings per owned path; the full-SHA scan on .github/workflows, .github/yaml and docker; root guards on the root; workflow guards on the workflow files; error ids on ci.sh; actionlint and shellcheck). Then each step fails once: lint returns 1 and every other step still ran. Five mutants of ci_cmd_lint are each killed at their own assertion: a dropped guard call, a swallowed failure in the middle and at the end, a stop after the first failure, a full-SHA dir dropped.
  • Release images (5e1cd37): the test grepped two of the six build calls and the runtime useradd, and checked the stop only for a failing make and apt install. It now asserts the exact ordered calls of both image steps (SOT apt packages in image mode, configure with --enable-Werror --without-system-popt, gated make, install into /out, make install into /out-pump, the pump rename, the nologin system user), and each step fails once: rc 1 and no later step ran; a failing nproc stops with rc 2 before make. Five mutants are each killed at their own assertion: configure not stopping, system popt, apt runner mode, a swallowed make install failure, a login shell for the distcc user.
  • Plan (7db73ae): the test compared ci_cmd_plan's phases with _ci_all_phases, which ci_cmd_plan itself calls, so a defect there changed both sides. A fixture SOT with three impact classes now gives the literal set build package verify. One table covers dispatch and schedule on current_dev, a dispatch on master (not tested before), bot/x and 544/merge, each row checking phases, build, matrix and publish_buildtools together; a new case checks that a SOT without verify and build never publishes buildtools and never builds a matrix. Six mutants are each killed at their own row: master not protected, every ref protected, publish without verify, build without a matrix, a matrix without build, an empty phase set.
  • Table loops: all 13 heredoc-driven table loops in ci.bats run every row (iterations counted against rows in an instrumented copy), so no loop body consumes its table from stdin.

Correction to the f643e31 commit message

It says the lint test had "six others" stubbed. Before f643e31 the test stubbed nine steps (ci_guard_line_endings, ci_guard_full_sha, ci_guard_pins_in_sot, ci_guard_sot_mirrors, ci_guard_orchestrator_only, ci_guard_job_timeouts, ci_guard_comment_format, _ci_lint_actionlint, _ci_lint_shellcheck); ci_guard_banned_texts, ci_guard_sot_pins, ci_guard_path_mirrors and ci_guard_error_ids ran for real. The pushed message is not rewritten.

[AG-CODE-004]

Each search ran on the parent of the introducing commit, over the 14 CI files tracked there.

  • I confirm I did not violate [AG-CODE-004] -- I searched f643e31~1 for a way to record and fail single steps and for existing lint tests (_record, _fail, ci_cmd_lint, read -r guard) and found the _record and _fail stubs and the one-guard lint test, so I am modifying that test in .github/scripts/ci.bats to record every lint step with _record and fail each with _fail.
  • I confirm I did not violate [AG-CODE-004] -- I searched 5e1cd37~1 for call-order assertions on the release image steps (_record, cat "${CALL_LOG}", _ci_image_release_build, _ci_image_release_runtime) and found the _record stub and the grep-based release image test, so I am modifying that test in .github/scripts/ci.bats to compare the full recorded call list.
  • I confirm I did not violate [AG-CODE-004] -- I searched 7db73ae~1 for fixture SOTs and plan tests (_fixture_manifest, _ci_all_phases, ci_cmd_plan) and found _fixture_manifest and the plan test that derived its phases from _ci_all_phases, so I am modifying that test in .github/scripts/ci.bats to use a _fixture_manifest SOT with a literal expectation.

djdomi added 3 commits October 8, 2026 13:12
…ixture

The impact test expected exact phase lists per path class from the
real SOT, a second copy of the SOT's routing: any routing edit broke it
without a defect. The real SOT now gets contract rows only: a docs-only
diff is NOOP, a C source runs build, and a SOT or ci.sh change selects
exactly the phases validate.yml gates on (read from its
contains(needs.plan.outputs.phases, ...) gates and the build gate).
The mechanics run on a fixture SOT: the sorted, de-duplicated union
over matched classes, a path in two classes, NOOP for unmatched or
phase-less classes; a class without a phases list and a failing
classifier are rc 2, never NOOP. Five mutants are each killed: no
sort or dedupe, no NOOP, a swallowed classifier error, a swallowed SOT
error, only the first class used.
…alone

ci_guard_sot_mirrors skipped its housekeeping.yml and dependabot.yml
checks silently when the file was absent, while its validate.yml and
CFL Dockerfile checks logged NotRun. Both now log "NotRun: <file>
absent" in the same form. No other existence check in ci.sh skips
silently.

The mirror test ran the guard on the real tree, which the lint job
already does, then planted every drift at once and only looked for
each id somewhere in the output. It now starts from a clean fixture
tree that must pass without any CI-ERROR, applies one drift per row,
and requires exactly that row's id (cron, package option, milestone
changed or missing, task option, unwired phase, CFL FROM), plus a
NotRun row for the absent files. Six mutants are each killed: either
NotRun line dropped, the 0002, 0009 or 0010 failure swallowed, the
milestone check raising the wrong id.
…reutils

Ten assertions inside table loops failed with a bare [ ... ] line, so
the failing row was not named (live PR fetch, metadata events,
self-compile passes, popt CVE ids, timeout guard args, attest claims);
the CI_COMMANDS test did not print the missing names. Each now names
its row. The verify-all / brew test gets a case line per part, and a
printf in the SOT reader test had a literal newline in its format.

The attest claims test meant to prove that _ci_attest_claims pads the
JWT payload before base64 -d. coreutils 9.5 stopped requiring padding,
and the buildtools image has 9.7, so removing the padding still passed.
The attest step runs on ubuntu-24.04 runners with coreutils 9.4, which
rejects unpadded input. The test now keeps what base64 -d receives and
requires a length divisible by 4, and two new claims encode to / and +
so the base64url alphabet swap is covered too. Both mutants (no
padding, no swap) are killed.
@djdomi

djdomi commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Update: 47800e5..4aea428 (impact, mirror guard, row labels, attest padding)

Follows #544 (comment).

Commits and real CI

Commit Change Validate / Security
47800e5 impact checks real-SOT contracts and the phase union on a fixture success 37768629411 / 37768629532
6a8a480 mirror guard reports absent mirrored files; test each drift alone pushed together with 4aea428, covered by its run
4aea428 table rows name themselves; attest proves padding on any coreutils success 37770739362 / 37770739351

Changes

  • Impact (47800e5): the test expected exact phase lists per path class from the real SOT, a second copy of the SOT's routing. Real-SOT rows are now contracts only: a docs-only diff is NOOP, a C source runs build, and a SOT or ci.sh change selects exactly the phases validate.yml gates on (read from its contains(needs.plan.outputs.phases, ...) gates and the build gate). The mechanics run on a fixture SOT (sorted, de-duplicated union over matched classes; a path in two classes; NOOP for unmatched or phase-less classes); a class without a phases list and a failing classifier are rc 2, never NOOP. Five mutants are each killed.
  • Mirror guard (6a8a480, ci.sh): an absent housekeeping.yml or dependabot.yml skipped its check without output, while the guard's other two mirrored files logged NotRun. Both now log NotRun: <file> absent. All 23 existence checks in ci.sh were read in context; no other silent skip. The test replaces its real-tree run (the lint job already runs the guard on the real tree) with a clean fixture tree that must pass without any CI-ERROR, then applies one drift per row and requires exactly that row's id; a NotRun row covers the absent files. Six mutants are each killed.
  • Row labels (4aea428): ten assertions inside table loops failed with a bare [ ... ] line and did not name the failing row; each now does. The verify-all / brew test gets a case line per part.
  • Attest padding (4aea428): the attest claims test was meant to prove that _ci_attest_claims pads the JWT payload before base64 -d. coreutils 9.5 (NEWS, 2024-03-28: "base32 and base64 no longer require padding when decoding") made that invisible in the buildtools image (coreutils 9.7): removing the padding still passed. The attest step runs on ubuntu-24.04 runners (e.g. job 113282442227, image 20261004.327.1), and Ubuntu 24.04 ships coreutils 9.4, which rejects unpadded input; so the padding is needed and was untested. The test now checks that base64 -d receives a length divisible by 4, and two new claims encode to / and +, covering the base64url alphabet swap. Both mutants (no padding, no swap) are killed.

ci.bats run time, measured

Same host, same buildtools image plus bats 1.11.1, three alternating runs of 7b6294d (218 tests) and the 4aea428 content (120 tests):

7b6294d 4aea428 median change
parallel (--jobs 16) 6041 / 6017 / 6063 ms 4118 / 4145 / 4400 ms -31%
serial 18227 / 18496 / 18242 ms 14167 / 13745 / 13680 ms -25%
sum of per-test times 11277 / 11450 / 11343 ms 10584 / 10275 / 10109 ms -9%

Most of the serial gain is per-test setup that 98 fewer declarations no longer pay. The merged tables are now the slowest tests (impact 614 ms, lint wiring 503 ms, mirror drift 419 ms, medians); the plan test dropped from 501 + 444 ms (two tests on the real SOT) to 304 ms.

Test image vs runner tools

The attest gap came from a tool that behaves differently in the test image than on the runner, so the CI tools were compared: buildtools has coreutils 9.7, mawk 1.3.4 20250131, git 2.47.3, curl 8.14.1; the ubuntu-24.04 runner has coreutils 9.4, mawk 1.3.4 20240123, git 2.55.0, curl 8.5.0 (sed 4.9 and jq 1.7 on both). The coreutils 9.5 to 9.7 behavior changes and the mawk changes in between were read in full against ci.sh: only the base64 padding change touches CI code. git, bash patch level and curl differences were not swept; curl and gh are stubbed in the tests, and the git paths run in real CI on every pull request.

Ledger state

All 220 rows of the ci.bats ledger resolve to a current test (own test, case line, absorbed row or renamed) or to a recorded deletion (3); the 120 tests are those homes plus the two new tests without a ledger row (checkout, path mirror guard). Rows still waiting on a maintainer decision: the draft exemption of the tracking gate, and the rows on retry classification and the image full-upgrade, whose scope is an open question.

[AG-CODE-004]

Each search ran on the parent of the introducing commit, over the 14 CI files tracked there.

  • I confirm I did not violate [AG-CODE-004] -- I searched 47800e5~1 for fixture SOTs and readers of the validate.yml phase gates (_fixture_manifest, _ci_phases_for_paths, contains(needs.plan.outputs.phases) and found _fixture_manifest and _ci_unwired_phases, which reads the same gates in the other direction (SOT phase to job) for the mirror guard; the test needs the gates as an expectation independent of that guard, so I am modifying the impact test in .github/scripts/ci.bats to use _fixture_manifest and to read the gates from validate.yml itself.
  • I confirm I did not violate [AG-CODE-004] -- I searched 6a8a480~1 for the NotRun form for an absent file (NotRun: ${f} absent, ci_guard_sot_mirrors) and found it in two branches of ci_guard_sot_mirrors and two of ci_guard_path_mirrors, so I am modifying ci_guard_sot_mirrors in .github/scripts/ci.sh to use the same line in its two remaining branches.
  • I confirm I did not violate [AG-CODE-004] -- I searched 4aea428~1 for a stub that keeps a command's stdin and still runs the real command (base64 -d, _ci_attest_claims, command base64, plus the _record/_fake_tool helpers) and found none (_record replaces the command, _fake_tool logs only arguments), so I am implementing a base64 wrapper inside the attest claims test in .github/scripts/ci.bats.

djdomi added 2 commits October 8, 2026 13:47
…nknown

ci_cmd_e2e piped _ci_sot_optional into grep -q to test whether a mode
exists. When the SOT read failed (e.g. the workload key holds a
section, CI-ERROR-SOT-0010), pipefail made the test false and the mode
was also reported as unknown (CI-ERROR-E2E-0013), a second, wrong
cause. The value is now read first: a read error returns 2 with only
the SOT id; an empty value is an unknown mode as before. This also
removes the pipe into an early-exit reader.

The unknown-command test gets a row for the unreadable entry. Two
mutants are killed: dropping the new return, and the old pipe form.
47800e5 replaced the real-SOT rows for a C source (build e2e package),
an include-server file (build e2e) and an unmatched path (NOOP) with
"a C source includes build". That weakened the check: removing package
from the c-source class in the SOT no longer failed any test. The
three exact rows are back, next to the fixture mechanics and the
validate.yml gate row. A SOT mutant without package in the c-source
phases fails at the c-source row.
@djdomi

djdomi commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Maintainer direction (2026-10-08): impact is content-based, not path-based

Recorded from the maintainer's review of this PR on 2026-10-08.

  • Every run starts at NOOP. Work needs a demonstrable reason in the content of the change.
  • A change to ci.sh, ci.bats or the SOT does not select every phase. A comment-only or formatting-only change is NOOP; a logic change selects only the work whose behavior it can change.
  • A docs-only change is NOOP. A change to test code runs the tests, not a build.
  • lint and the engine self-test follow the same rule. A check that scans the whole repository instead of the diff needs a stated reason.
  • Expected values in ci.bats come from their owner or from fixtures, never from hard-coded copies of SOT data or of real product paths.
  • A run that needs no build finishes within seconds to a few minutes. Today every plan also runs lint (50 s) and engine-selftest (20 s) besides plan (11 s), metadata (6 s) and the gate (4 s) (Validate 37772453181).

Current state against this direction

  • ci.sh impact and plan decide by path only: any edit to a path in an impact class selects its phases, comment-only edits included. The sot and ci-engine classes select all five phases. lint and engine-selftest run on every plan. This does not meet the direction; it is being changed in this PR.
  • The impact test's real-SOT rows hard-coded product paths (src/dopt.c, include_server/basics.py) and copied the SOT's phase lists. They are being removed in favor of fixtures and owner-derived values.
  • Correction to feat(ci): CI Rewrite 1.2 with one engine, one SOT and five thin workflows #544 (comment): its requirement table reports "impact: diff -> classes -> phases, DEFAULT=NOOP | met". That is not correct; see above.

djdomi added 2 commits October 8, 2026 14:20
80e192f restored real-SOT rows that hard-coded product paths
(src/dopt.c, include_server/basics.py, LICENSE) and copied the SOT's
phase lists. Issue #479 asks for fixtures, and copied SOT data only
proves that two copies agree; the routing per path class is owned by
the SOT itself. These rows are removed.

The one routing contract with a binding source is that a docs-only
diff selects nothing (DEFAULT=NOOP). The test now takes every tracked
file that the SOT's labels.documentation map names (one git ls-files
with the SOT globs as pathspecs, no path written in the test) and
requires NOOP for that diff. A SOT mutant giving the docs class a
build phase fails it.
DEFAULT=NOOP: a comment- or format-only edit must select no work, but
ci.sh impact (and impact-hit) selected a class's phases for any edit
to one of its paths. Each changed path is now compared at base and
head after normalization by the real parser for its type:

- shell and bats: bash parses the file as a function body and prints
  it with declare -f (comments dropped, heredocs and strings kept,
  nothing executed); the shebang is compared on its own;
- C sources and headers: gcc -fpreprocessed strips comments; an added
  line with /*, */, ??/ or a trailing backslash still counts, since a
  comment edit like that can break a -Werror build (-Wcomment);
- the SOT: the index of its own reader (_ci_sot_dump);
- Dockerfiles: comment lines dropped, parser directives kept, a file
  with a heredoc always counts.

Line endings are ignored. Any other file type, and any added, deleted
or unparsable file, counts on every edit (unsure means changed).

The OSV PR gate read the base SOT with its own ls-tree/show code and a
literal SOT path; both now use the shared _ci_rev_file and
_ci_sot_relpath. The impact test gets a 19-row table on a fixture
repo; eight mutants of the filter are each killed at their own row.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and runner workflow changes documentation Improvements or additions to documentation enhancement New feature or request packaging Component: RPM/deb/tarball/container release packaging

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

CI Rewrite 1.2

1 participant