forjar 1.30.0

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
# Sovereign CI — calls reusable workflow from paiml/.github
# Change once in paiml/.github → applies to all repos
#
# Jobs provided by sovereign-ci.yml:
#   test: cargo test --lib (self-hosted clean-room)
#   lint: cargo clippy --all-targets -- -D warnings + cargo fmt --check
#   coverage: cargo llvm-cov + codecov upload
#   security: cargo audit (ubuntu-latest, continue-on-error)
#   provenance: SLSA attest-build-provenance
#   gate: aggregates test+lint results
#
#   NOTE: sovereign-ci compiles NO doctests. Its test job is hard-scoped to
#   `cargo test --lib` and, with use_nextest: true, is run by cargo-nextest,
#   which cannot execute doctests at all. See the `doctests` job below (#318).
name: CI

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]
  workflow_dispatch:

concurrency:
  group: ci-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  # PMAT-237: what can this change break? One job, one decision, and every
  # heavy job below gates on it. The release gate (`make dogfood-release`) is
  # untouched and still runs everything.
  classify:
    runs-on: ubuntu-latest
    outputs:
      code: ${{ steps.class.outputs.code }}
      gate_c: ${{ steps.class.outputs.gate_c }}
      gate_d: ${{ steps.class.outputs.gate_d }}
      gates: ${{ steps.class.outputs.gates }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
        with:
          fetch-depth: 0
      - id: class
        uses: ./.github/actions/changed-class

  ci:
    if: needs.classify.outputs.code == 'true'
    needs: classify
    uses: paiml/.github/.github/workflows/sovereign-ci.yml@main
    with:
      repo: ${{ github.event.repository.name }}
      # PMAT-155 Phase 2 nextest pilot (build-performance.md §4.3 + §7 Phase 2).
      # forjar = medium workload, healthy --lib suite. cargo-nextest is baked into
      # sovereign-ci:stable (infra Dockerfile). Pilot only — keep until F11 test-job
      # p95 ≤ 300s is verified over 7 days, then promote fleet-wide.
      use_nextest: true
    secrets: inherit

  # Release preflight: a stale Cargo.lock must fail PR CI, not the tag.
  # Every v1.4.x release failed because Cargo.toml was bumped without
  # committing the refreshed Cargo.lock — release.yml's `cargo package`
  # then dirties the tree and skips release creation entirely.
  lockfile:
    name: lockfile-preflight
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      - name: Verify Cargo.lock is committed and current
        # #423: the contract crates are workspace members, so the root crate's
        # path dependencies are unpublished until the release publishes them in
        # order. `--workspace` packages the members first and resolves the root
        # against that overlay (cargo >= 1.90), which is exactly what
        # `cargo publish --workspace` does at release time.
        run: cargo package --locked --no-verify --workspace

  # Regression guard (#179): every shipped standalone example config must pass
  # `forjar validate`. The sovereign-ci `test` job only runs `cargo test --lib`,
  # so this integration test (tests/examples_validate.rs) needs an explicit step.
  # Deterministic and offline — validate never opens a network/SSH connection.
  #
  # THAT `--lib` IS THE POINT OF THIS JOB, and it generalises: NOTHING in this
  # repo's CI runs `tests/*.rs` except the targets named here by hand. A
  # falsification test under tests/ that is not in this list is written, is
  # green on a developer's machine, and is never executed by CI again — which
  # is the same "guard nobody runs" shape as #242 and #298. Add new structural
  # guards to this list, or accept that they are documentation.
  examples-validate:
    if: needs.classify.outputs.code == 'true'
    needs: classify
    name: examples-validate
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      - name: Validate all standalone example configs
        run: cargo test --locked --test examples_validate
      # #292: a .rs file under src/ that no `mod`, `#[path]` or `include!`
      # names is compiled by nothing — not linted, not type-checked, and its
      # tests do not run. Three were in that state, one of which did not parse
      # as Rust at all. Cheap: a directory walk, no build of the crate needed.
      - name: No source file may be orphaned or duplicated
        run: cargo test --locked --test falsification_no_orphaned_source_files
      # #298: every citation in contracts/ must resolve to the exact item it
      # names, in the file it names, and every binding must name an equation
      # its contract defines. This replaces the Python resolver that used to
      # live in proofs.yml, whose regex read 77 of 211 citations and reported
      # "every resolvable falsifier citation resolves" over seven dangling
      # ones. One resolver, in one dialect, in a place that runs it.
      - name: Every contract citation must resolve
        run: cargo test --locked --test falsification_contract_citations_resolve
      # forjar#549: a drift query the target never answered is UNMEASURED, not
      # drift. Asks 203.0.113.9 (TEST-NET-3) over ssh, so it waits out the
      # connect timeout once per test.
      - name: An unanswered drift query is not drift
        run: cargo test --locked --test falsification_drift_unmeasured_is_not_drift
      # FALSIFY-FVS-005. Spawns the built binary, so it is the slow one here.
      - name: The binary advertises no undeclared transport
        run: cargo test --locked --test e2e_transport_absence_t
      # #376: `undo` re-converged to the CURRENT config instead of the target
      # generation, so it exited 0 printing "1 converged" while the managed
      # bytes never moved. These assert the BYTES AT THE PATH, never the
      # summary line — the summary is what the defect printed. Includes the
      # control that fails an "always rewrite" implementation, and the jidoka
      # cases: an undo that cannot faithfully replay must exit non-zero and
      # leave the host untouched.
      - name: undo must actually undo, or refuse
        run: cargo test --locked --test falsification_undo_actually_undoes
      # #377: `undo` paired the cwd forjar.yaml with an unrelated --state-dir
      # and applied it. Carries the over-correction guards too: one operator
      # editing and undoing their own stack must NEVER be refused.
      - name: undo must refuse a foreign state dir, and only a foreign one
        run: cargo test --locked --test falsification_undo_state_dir_interlock
      # #386: the Coverage lane cached `target`, and `cargo llvm-cov` over this
      # crate's 242 integration test binaries is a 70.70 GiB tree — already the
      # size of a hosted runner's disk. The cache asked tar+zstd for a second
      # copy of it on the same filesystem: the SAVE ENOSPC'd on every run (a
      # `##[warning]`, so the job stayed green and the cache was never written),
      # and when the margin was tighter the ENOSPC landed mid-build and killed
      # the runner's Worker process, taking the job's logs with it. Parses the
      # workflows and prints its denominator; self-hosted jobs are exempt.
      - name: No hosted job may cache a Rust build directory
        run: cargo test --locked --test falsification_hosted_jobs_do_not_cache_target
      # #401: fourteen paths were BOTH tracked and matched by .gitignore
      # (3,759,246 bytes). Nothing visibly broke — gitignore does not un-track a
      # file — which is exactly why it survived, while the post-commit hook
      # re-staged a 1.2 MB `.pmat/baseline.json` after every commit and any two
      # branches conflicted on its `created_at` line. Asserts the index against
      # the rules, with `--no-index` (without it git answers about the index and
      # reports every tracked path as not-ignored, hiding the defect) and
      # WITHOUT `-v` (which prints negation matches and exits 0, making the
      # assertion unsatisfiable while any file is deliberately re-included).
      - name: No tracked file may be gitignored
        run: cargo test --locked --test falsification_no_tracked_file_is_gitignored
      # #400: the quorum gate took the branch NAME from the pushed ref and
      # everything else — diff, receipt, falsification test — from the local
      # checkout. Pushing a branch you were not standing on was refused with
      # "no quorum receipt" for a receipt that was in the pushed commit, and an
      # UNTRACKED receipt carrying a `waived.reason` bypassed the whole gate
      # silently, for exactly the enforced authors QUORUM_SKIP is refused to.
      # Drives the real scripts/quorum-gate.sh against synthetic repos.
      - name: The quorum gate must judge the pushed commit
        run: cargo test --locked --test falsification_quorum_gate_reads_the_pushed_ref
      # PMAT-159: the sudo transport moved from `sudo bash /dev/fd/3` (sudo closes
      # fd 3 -> exit 127 for every sudo: true resource) to a private temp file. This
      # is the ALWAYS-ON falsifier: a fake sudo that closes every fd >= 3 and a fake
      # id that forces the non-root branch, then the real emitted wrapper. It went
      # RED on the old emitter and needs no privilege. The reusable test job is
      # `cargo test --lib`, so it runs here or nowhere.
      - name: The sudo transport survives closefrom
        run: cargo test --locked --test falsification_sudo_transport_closefrom_emulated
      # And the LIVE half of the same falsifier. The emulated test fakes `sudo`,
      # so it can show the transport surviving an emulated closefrom but never
      # the host's real one with a real uid 0 on the other side.
      # tests/falsification_sudo_transport_survives_closefrom.rs does exactly
      # that, but every test in it is gated on `sudo -n true` and SKIPS where
      # that is missing — and a skip is an absence of evidence: on a host
      # without passwordless sudo that file was green against the fd-3 emitter
      # too. FORJAR_REQUIRE_SUDO_TESTS=1 turns each of those skips into a panic
      # naming the missing capability, so a green step here means the gated
      # tests RAN, under real sudo, not that they declined to. ubuntu-latest
      # satisfies the gate by GitHub's documented hosted-runner environment
      # (the `runner` user is non-root and has passwordless sudo) — that is
      # cited, not measured here, which is why the switch is set: if it ever
      # stops holding, this step fails loudly instead of skipping silently.
      - name: The sudo transport survives closefrom under real sudo (required, not skippable)
        env:
          FORJAR_REQUIRE_SUDO_TESTS: "1"
        run: cargo test --locked --test falsification_sudo_transport_survives_closefrom
      # PMAT-159: a release ships ONE version and every artefact that states it
      # must state the same one — manifest, lockfile, changelog, and the binary's
      # own `--version`. The target existed and ran in NO CI job, which is the
      # same absence as not having it. `--locked` is the load-bearing flag, not
      # house style: under a bare `cargo test` cargo repairs a stale `Cargo.lock`
      # before the test body runs, so the #131 lockfile invariant cannot go red
      # here; `--locked` makes cargo refuse a lagging lock at resolution and the
      # test is the message-bearing witness for it. Drop the flag and this step
      # measures three of its four legs.
      - name: The release version is coherent (manifest, lock, changelog, binary)
        run: cargo test --locked --test falsification_version_matches_manifest

  # Regression guard (#237): `default-features = false` must yield a LIBRARY that
  # compiles. tests/falsification_default_features_trim.rs pins the manifest's
  # shape, but only a real build proves the module gates are consistent — trait
  # impls, macro expansions and glob re-exports are invisible to a manifest test.
  # The gates are load-bearing in one direction only: dropping `cli` must not
  # break the library, and nothing in the default lane would ever notice if it did.
  no-default-features:
    if: needs.classify.outputs.code == 'true'
    needs: classify
    name: no-default-features
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      - name: Library must build with every default feature off
        run: cargo check --locked --no-default-features --lib

  # Regression guard (#318): no PR job compiled a single one of this crate's 87
  # doctests. sovereign-ci's `test` is hard-scoped to `cargo test --lib` and, with
  # use_nextest: true, is run by cargo-nextest, which cannot execute doctests at
  # all; `lockfile` is --no-verify and `examples-validate` selects one integration
  # target. Doctests were therefore first compiled at the clean-room release gate
  # (GATE B3), where one wrong ``` fence costs a full release cycle — which is how
  # paiml/forjar#315 passed every PR check and then failed the release.
  # sovereign-ci has no doctest input and `--doc` cannot ride in on `test_args`,
  # because it conflicts with that job's `--lib` target selection. So the PR lane
  # needs its own job. Deterministic and offline; ~8s of doctests once built.
  doctests:
    if: needs.classify.outputs.code == 'true'
    needs: classify
    name: doctests
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      - name: Compile and run every doctest
        run: cargo test --locked --doc

  # Top-level gate: satisfies org ruleset which requires check named "gate".
  # The reusable workflow produces "ci / gate" but rulesets need exact match on "gate".
  # PMAT-163: the tool-free dogfood gates run here on every PR — surface
  # derived from the built artifact (C) and every documented invocation
  # executed against fixtures (D), plus the guard tests. Gates B (pmat comply),
  # F (coverage + mutants), G (pv contracts) and H (crux reconcile) need pmat
  # and pv, which are provisioned on the clean-room hosts by forjar.yaml, so
  # they run there through `make dogfood-release`.
  # PMAT-542: the gates that MEASURE THE BUILT BINARY, and the release build
  # they need, run only when the change can move them. Measured on the 25 PRs
  # merged as of cddf78cd: 7 were code=true and could reach neither gate, and
  # each paid 21.3 minutes of a 25.2-minute critical path for a surface its diff
  # could not touch. The guard tests below are a separate job because they are
  # cheap and every code change can move them.
  #
  # THIS IS A LATENCY CHANGE, NOT A COVERAGE CHANGE. `make dogfood-release` runs
  # A-H and T over the whole window before any tag, and the `gate` job below
  # refuses this job's skip whenever `gates` says it should have run.
  dogfood-surface:
    if: needs.classify.outputs.code == 'true' && needs.classify.outputs.gates != 'none'
    needs: classify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      - name: Build the artifact the surface gates measure
        run: cargo build --release --locked
      - name: Gate C — surface from the built artifact
        if: needs.classify.outputs.gate_c == 'true'
        run: bash scripts/dogfood/surface.sh
      - name: Gate C — NOT-SELECTED
        if: needs.classify.outputs.gate_c != 'true'
        run: echo "GATE C NOT-SELECTED this change touches nothing gate C reads (the built binary, docs/audits/surface_audit.csv, scripts/dogfood/surface.sh) — measured in full by make dogfood-release before the tag"
      - name: Cookbook — clone paiml/forjar-cookbook (public) beside the workspace for gate D
        if: needs.classify.outputs.gate_d == 'true'
        run: git clone --quiet --depth 1 --branch master https://github.com/paiml/forjar-cookbook "$RUNNER_TEMP/forjar-cookbook"
      - name: Gate D — documented invocations run against fixtures
        if: needs.classify.outputs.gate_d == 'true'
        run: bash scripts/dogfood/docs.sh
        env:
          COOKBOOK: ${{ runner.temp }}/forjar-cookbook
      - name: Gate D — NOT-SELECTED
        if: needs.classify.outputs.gate_d != 'true'
        run: echo "GATE D NOT-SELECTED this change touches nothing gate D reads (the built binary, README.md, docs/audits/surface_audit.csv, scripts/dogfood/docs.sh) — measured in full by make dogfood-release before the tag"

  dogfood-guards:
    if: needs.classify.outputs.code == 'true'
    needs: classify
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
      - name: Guard tests
        run: cargo test --locked --test falsification_dogfood_skill_is_named --test falsification_dogfood_scripts_declare_mutations --test falsification_dogfood_release_check_pr_window --test falsification_dogfood_harness_and_quorum --test falsification_crux_gate_reads_the_release_section --test falsification_coverage_gate_mutation_scope --test falsification_cb200_ratchet_measures_this_tree --test falsification_pr_lane_runs_what_the_change_can_break --test falsification_pr_lane_selects_the_gate_the_change_can_move --test falsification_gate_a_the_pr_and_its_commits_name_one_ticket

  gate:
    runs-on: ubuntu-latest
    needs: [classify, ci, lockfile, examples-validate, no-default-features, doctests, dogfood-surface, dogfood-guards]
    if: always()
    steps:
      - name: Check required jobs
        env:
          CODE: ${{ needs.classify.outputs.code }}
          GATES: ${{ needs.classify.outputs.gates }}
        run: |
          # PMAT-237: a job may be SKIPPED only because the change cannot reach
          # it. The gate re-reads the class rather than trusting the skip: if
          # the change IS code and a heavy job did not run, that is a hole, not
          # a saving, and this gate refuses it by name.
          # Written with explicit `if`, not `[ … ] && return 0`: an AND-list
          # that fails at statement level exits the step under `bash -e` before
          # the message is printed, and a gate that fails silently is half a
          # gate.
          # THE CLASS ITSELF MUST HAVE BEEN MEASURED (PMAT-237).
          #
          # If `classify` fails or is cancelled its output is EMPTY, every heavy
          # job's `if` is false so they all skip, and a gate that read an empty
          # class as "not code" would pass over a change nothing tested. A
          # review lane found exactly that. An unmeasured class is code.
          if [ "${{ needs.classify.result }}" != "success" ]; then
            echo "::error::classify did not run (${{ needs.classify.result }}), so nothing knows what this change can break"
            exit 1
          fi
          if [ -z "$CODE" ]; then
            echo "::error::the class is empty — treating it as code and refusing"
            exit 1
          fi
          # PMAT-542: the SELECTION must have been measured too. The classifier
          # prints `none` and never the empty string, exactly so that "nothing
          # was selected" and "nothing computed a selection" cannot look the
          # same here. An empty value is the second one.
          if [ -z "$GATES" ]; then
            echo "::error::the gate selection is empty — nothing measured which gates this change can move, and an unmeasured selection is not a selection"
            exit 1
          fi

          ok() {
            case "$2" in
              success) return 0 ;;
              skipped)
                if [ "$CODE" != "true" ]; then
                  echo "$1: skipped — the change touches nothing it reads"
                  return 0
                fi
                echo "::error::$1 was skipped on a change the classifier called code"
                return 1
                ;;
              *)
                echo "::error::$1: $2"
                return 1
                ;;
            esac
          }
          ok "ci" "${{ needs.ci.result }}" || exit 1
          if [ "${{ needs.lockfile.result }}" != "success" ]; then
            echo "lockfile preflight failed: ${{ needs.lockfile.result }}"
            exit 1
          fi
          ok "examples-validate" "${{ needs.examples-validate.result }}" || exit 1
          ok "no-default-features" "${{ needs.no-default-features.result }}" || exit 1
          ok "doctests" "${{ needs.doctests.result }}" || exit 1
          ok "dogfood-guards" "${{ needs.dogfood-guards.result }}" || exit 1
          # PMAT-542: dogfood-surface skips on its OWN condition, which is
          # `gates != none` rather than the class. Re-read the selection here
          # for the same reason `ok` re-reads the class: a skip is a claim until
          # the thing that decided it says so.
          case "${{ needs.dogfood-surface.result }}" in
            success) echo "dogfood-surface: ran gates $GATES" ;;
            skipped)
              if [ "$CODE" != "true" ]; then
                echo "dogfood-surface: skipped — the change touches nothing it reads"
              elif [ "$GATES" = "none" ]; then
                echo "dogfood-surface: NOT-SELECTED — this change can move neither gate C nor gate D; both are measured in full by make dogfood-release before the tag"
              else
                echo "::error::dogfood-surface was skipped while the selection says $GATES"
                exit 1
              fi
              ;;
            *)
              echo "::error::dogfood-surface: ${{ needs.dogfood-surface.result }}"
              exit 1
              ;;
          esac
          echo "All required jobs passed"