name: jouleclaw public surface must stay documented # Regression-prevention gate for jouleclaw-rs. The brownfield # onramp (`cargo run -p jouleclaw-brownfield --example # scan_self`) walks every crate, infers reasons for every # public function (docs / name pattern / error-handling # signal), and reports those whose best-available reason is # Lost. After the 219 → 0 curation pass that closed the # initial baseline, every new `pub fn` is expected to ship # with the same minimal `///` doc, OR be downgraded to # `pub(crate)` if it isn't part of the consumer-visible # surface. The `--fail-on-lost` flag exits non-zero when any # public-surface Lost reason remains, so this workflow is a # permanent safety net against drift. # A pull request's newer push supersedes its older run; a push to main does not. # # On a pull request only the newest commit's verdict is wanted. With no # concurrency group, a session pushing four versions of a branch queued four # complete sets of runs and every one of them had to drain: on 2026-10-05 a PR's # 21 contexts settled none in 70 minutes behind a backlog of 25 runs, and the # queue grew faster than it cleared. So a pull request's older runs are # cancelled. # # Verified on this forge (Forgejo 16.0.5) in `ci-test` before being added here: # the key parses, and a newer push moves an older run to `cancelled` even while # it is still queued, which is the case that matters. # # A push to main takes a group of its own, its commit, so no later push cancels # its run. Work lands as direct commits to main, several an hour, and Forgejo # hands runners the oldest queued job first, so a run the next push cancelled # was replaced at the back of the queue: while the linux-arm64 queue was hours # deep, a workflow touched more often than that never reached the front. In the # day to 2026-11-10 01:11Z, 578 of the 810 runs main's pushes started were # cancelled; joule-moq-room's last run on main to finish ended at 13:58Z on # 11-09, and 31 of the 32 after it were cancelled. `cancel-in-progress: false` # would not help: Forgejo then holds each run of a group until the one before it # has finished, and keeps every one waiting. Forgejo cancels a job left waiting # 24 hours. concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: true on: # Manual re-run, so a transient runner/setup failure (which surfaces as a red # "audit" even though the audit itself never ran) can be retried without a # code change. workflow_dispatch: pull_request: paths: - 'jouleclaw/jouleclaw-rs/**' - '.github/workflows/jouleclaw-public-surface-documented.yml' push: branches: - main paths: - 'jouleclaw/jouleclaw-rs/**' - '.github/workflows/jouleclaw-public-surface-documented.yml' jobs: scan: name: Public-surface audit (jouleclaw-rs) runs-on: linux steps: - name: Checkout uses: actions/checkout@v4 # Bootstrap rustup: the docker runner image ships no Rust, and # dtolnay/rust-toolchain cannot help — it calls `rustup`, which is absent. - name: Install Rust 1.99.1 run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.99.1 rustup toolchain install 1.99.1 --profile minimal rustup default 1.99.1 echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Cache cargo registry + target uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git jouleclaw/jouleclaw-rs/target key: ${{ runner.os }}-cargo-brownfield-${{ hashFiles('jouleclaw/jouleclaw-rs/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-brownfield- - name: Run brownfield self-scan with --fail-on-lost working-directory: jouleclaw/jouleclaw-rs # The report (stdout) goes to a file, not the log: it is about 800 KB # written at once, and on 2026-11-07 three runs of it, on three # runners, kept a log that stopped at 256 KiB, mid-report, and the job # was failed twelve minutes later, though the scan's own summary read # "public surface only: 0". The summary (stderr) stays in the log, and # on a failure so does the report's list of what is missing, which # comes before its module purposes. run: | set -euo pipefail report="$(mktemp)" if ! cargo run -q -p jouleclaw-brownfield --example scan_self -- --fail-on-lost > "$report"; then sed -n '/^## Module purposes/q;p' "$report" echo '::error::Public-surface Lost > 0 — jouleclaw-rs has undocumented public functions.' echo '::error::Fix by adding a `///` doc comment above each flagged `pub fn`, OR by downgrading to `pub(crate)` if it is not consumer-visible.' echo '::error::Run locally with: cargo run -p jouleclaw-brownfield --example scan_self' exit 1 fi echo "public-surface report: $(wc -c < "$report") bytes, not printed; run the scan locally to read it"