Files
ironclaw/.github/workflows/code_style.yml
dependabot[bot] c1db630b1f chore(deps): bump the actions group across 1 directory with 5 updates
Bumps the actions group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) | `1.0.183` | `1.0.208` |
| [actions/setup-node](https://github.com/actions/setup-node) | `4.0.2` | `7.0.0` |
| [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) | `e18b497796c12c097a38f9edb9d0641fb99eee32` | `f0d9c3887740aee45f6153b24b3a6b815192ec16` |
| [docker/login-action](https://github.com/docker/login-action) | `4.5.2` | `4.6.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.2.0` | `4.3.0` |



Updates `anthropics/claude-code-action` from 1.0.183 to 1.0.208
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](be7b93b190...e8c2d7c16c)

Updates `actions/setup-node` from 4.0.2 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.2...v7)

Updates `Swatinem/rust-cache` from e18b497796c12c097a38f9edb9d0641fb99eee32 to f0d9c3887740aee45f6153b24b3a6b815192ec16
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](e18b497796...f0d9c38877)

Updates `docker/login-action` from 4.5.2 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](371161bbe7...dbcb813823)

Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](bb05f3f551...37fe631027)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.198
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: Swatinem/rust-cache
  dependency-version: f0d9c3887740aee45f6153b24b3a6b815192ec16
  dependency-type: direct:production
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 20:16:14 +00:00

735 lines
37 KiB
YAML

name: Code Style
on:
pull_request:
merge_group:
branches:
- main
types:
- checks_requested
# Pushes to main refresh the rust-cache entries that PR and merge-group
# jobs restore from. Non-push runs stay restore-only.
push:
branches:
- main
permissions:
contents: read
concurrency:
group: code-style-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
# Tests must never touch the real OS keychain (macOS Keychain auth dialog /
# Linux Secret Service). Guarded by
# crates/substrates/ironclaw_secrets/src/keychain.rs::os_keychain_suppressed:
# cfg!(test) covers unit tests; this covers integration/e2e that link the
# non-cfg(test) library.
IRONCLAW_DISABLE_OS_KEYCHAIN: "1"
# Pin wall-clock rendering inputs so a test cannot pass or fail on the
# runner's locale. Reborn renders model-visible time slices, and date/number
# formatting differs by locale, so an unpinned runner turns a formatting
# regression into an unreproducible flake. UTC also keeps recorded fixtures
# comparable with what a developer sees locally.
TZ: "UTC"
LANG: "C.UTF-8"
jobs:
changes:
name: Detect code changes
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
has_code: ${{ steps.non_pr.outputs.has_code || steps.diff.outputs.has_code }}
has_guidance: ${{ steps.non_pr.outputs.has_guidance || steps.diff.outputs.has_guidance }}
has_docs: ${{ steps.non_pr.outputs.has_docs || steps.diff.outputs.has_docs }}
has_reborn_cli: ${{ steps.non_pr.outputs.has_reborn_cli || steps.diff.outputs.has_reborn_cli }}
has_clippy: ${{ steps.non_pr.outputs.has_clippy || steps.diff.outputs.has_clippy }}
clippy_scope: ${{ steps.non_pr.outputs.clippy_scope || steps.diff.outputs.clippy_scope }}
clippy_packages: ${{ steps.non_pr.outputs.clippy_packages || steps.diff.outputs.clippy_packages }}
clippy_matrix: ${{ steps.non_pr.outputs.clippy_matrix || steps.diff.outputs.clippy_matrix }}
steps:
- id: non_pr
if: github.event_name == 'push'
run: |
echo "has_code=true" >> "$GITHUB_OUTPUT"
echo "has_guidance=true" >> "$GITHUB_OUTPUT"
echo "has_docs=true" >> "$GITHUB_OUTPUT"
echo "has_reborn_cli=true" >> "$GITHUB_OUTPUT"
echo "has_clippy=true" >> "$GITHUB_OUTPUT"
echo "clippy_scope=full" >> "$GITHUB_OUTPUT"
echo 'clippy_packages=[]' >> "$GITHUB_OUTPUT"
echo 'clippy_matrix=[{"name":"all-features","flags":"--all-features"},{"name":"default","flags":""}]' >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
with:
# PR scope keeps full ancestry for its three-dot diff. A merge-group
# checkout is the candidate commit only; its base is fetched below.
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
filter: blob:none
persist-credentials: false
- name: Fetch merge-group base for scope diff
if: github.event_name == 'merge_group'
env:
BASE_SHA: ${{ github.event.merge_group.base_sha }}
run: git fetch --no-tags --filter=blob:none --depth=1 origin "$BASE_SHA"
- id: diff
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
if [[ "${{ github.event_name }}" == "merge_group" ]]; then
CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")"
else
CHANGED_FILES="$(git diff --name-only "$BASE_SHA"..."$HEAD_SHA")"
fi
printf '%s\n' "$CHANGED_FILES" > "${RUNNER_TEMP}/changed-files.txt"
clippy_plan="$(
python3 scripts/ci/changed_workspace_packages.py \
--event "${{ github.event_name }}" \
--changed-files "${RUNNER_TEMP}/changed-files.txt"
)"
clippy_scope="$(jq -r '.mode' <<< "${clippy_plan}")"
clippy_packages="$(jq -c '.packages' <<< "${clippy_plan}")"
echo "clippy_scope=${clippy_scope}" >> "$GITHUB_OUTPUT"
echo "clippy_packages=${clippy_packages}" >> "$GITHUB_OUTPUT"
if [[ "${clippy_scope}" != "none" ]]; then
echo "has_clippy=true" >> "$GITHUB_OUTPUT"
else
echo "has_clippy=false" >> "$GITHUB_OUTPUT"
fi
if [[ "${clippy_scope}" == "full" ]] || \
printf '%s\n' "$CHANGED_FILES" | grep -Eq '^(crates/|tests/|migrations/|Cargo\.toml$|Cargo\.lock$|deny\.toml$|Dockerfile|\.gitignore$|scripts/ci/|\.githooks/|docker/reborn/(entrypoint|start-sshd)\.sh$|scripts/reborn_webui_v2_live_qa/|scripts/live-canary/|\.github/workflows/(live-canary|reborn-e2e)\.yml$|scripts/check_no_panics\.py$|scripts/no_panics_reborn_baseline\.txt$|scripts/(check|test-check)-type-duplicates\.py$|\.github/scripts/(pr-labeler|test-pr-labeler)\.sh$|\.github/workflows/(code_style|main-ci-slack-alerts)\.yml$|\.github/workflows/ironclaw-release\.yml$|rust-toolchain\.toml$|\.github/actions/setup-rust/)'; then
echo "has_code=true" >> "$GITHUB_OUTPUT"
else
echo "has_code=false" >> "$GITHUB_OUTPUT"
echo "No code changes — style checks will be skipped"
fi
# The guidance gate (scripts/ci/check-guidance.py, run inside
# fast-checks) scans surfaces the code filter above deliberately
# leaves out: `.claude/` rules and skills (whose frontmatter
# `paths:` triggers it verifies), the root AGENTS.md/CLAUDE.md pair
# (reference seed + alias site), and `docs/` (the reference-target
# space guidance most often cites — a rename there must dangle
# references loudly, not merge green). Without this output a PR
# editing only `.claude/rules/` would skip the exact
# never-firing-trigger class the gate exists to catch (#7306
# review). `has_code` keeps its meaning — ws12_workflow_contracts.py
# pins docs/ and openwiki/ OUT of it — so this OR-s into
# fast-checks' condition only; the clippy and JS lanes stay
# code-scoped. openwiki/ stays out here too: it is generated by its
# own workflow and is not a check-guidance scan surface.
if printf '%s\n' "$CHANGED_FILES" | grep -Eq '^(\.claude/|AGENTS\.md$|CLAUDE\.md$|docs/)'; then
echo "has_guidance=true" >> "$GITHUB_OUTPUT"
else
echo "has_guidance=false" >> "$GITHUB_OUTPUT"
fi
# docs/ is deliberately NOT in the has_code scope above, so a
# docs-only PR skips every Rust lane — the publication-boundary gate
# needs its own trigger. The gate's own files are in scope so the
# guardrail runs when it changes (review-discipline.md: "Guardrails
# are code").
if printf '%s\n' "$CHANGED_FILES" | grep -Eq '^(docs/|scripts/ci/docs_publication_boundary\.py$|scripts/ci/test_docs_publication_boundary\.py$|\.github/workflows/code_style\.yml$)'; then
echo "has_docs=true" >> "$GITHUB_OUTPUT"
else
echo "has_docs=false" >> "$GITHUB_OUTPUT"
fi
# Crate scopes are matched by NAME at any depth: `crates/([^/]+/)*<name>/`
# keeps matching once crates move into family directories
# (crates/<family>/ironclaw_*, PROPOSAL §5). A flat `crates/<name>/`
# prefix stops matching there, `has_reborn_cli` goes false, and the
# dist-build lane silently skips — the WS10 failure mode (#6963).
# scripts/ci/ws12_workflow_contracts.py pins this regex against the
# real crate inventory, so a renamed or moved crate fails loudly here
# instead of quietly falling out of scope.
# `ironclaw_loop_host` joined the list with the WS3 runner sheds: the
# model gateway and the tool-disclosure decorator moved there out of
# `ironclaw_turn_runner`, so without it a PR touching only the new home
# would no longer light this lane — the move would have narrowed CI
# scope as a side effect.
if printf '%s\n' "$CHANGED_FILES" | grep -Eq '^(crates/([^/]+/)*ironclaw_turn_runner/|crates/([^/]+/)*ironclaw_loop_host/|crates/([^/]+/)*ironclaw_cli/|crates/([^/]+/)*ironclaw_config/|crates/([^/]+/)*ironclaw_architecture_tests/tests/reborn_dependency_boundaries\.rs$|scripts/ci/smoke-release-binary\.py$|tests/test_smoke_release_binary\.py$|Cargo\.toml$|Cargo\.lock$|\.github/dist-build-setup\.yml$|\.github/workflows/(code_style|ironclaw-release|docker|reborn-release-compile)\.yml$)'; then
echo "has_reborn_cli=true" >> "$GITHUB_OUTPUT"
else
echo "has_reborn_cli=false" >> "$GITHUB_OUTPUT"
fi
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo 'clippy_matrix=[{"name":"all-features","flags":"--all-features"}]' >> "$GITHUB_OUTPUT"
else
echo 'clippy_matrix=[{"name":"all-features","flags":"--all-features"},{"name":"default","flags":""}]' >> "$GITHUB_OUTPUT"
fi
fast-checks:
name: Fast deterministic checks
needs: changes
# `has_guidance` OR-s in the guidance surfaces (.claude/, root
# AGENTS.md/CLAUDE.md, docs/) so check-guidance.py runs for the files it
# governs. Clippy additionally follows `clippy_scope=full` for merge-group
# inputs that are outside the narrow `has_code` path filter.
if: needs.changes.outputs.has_code == 'true' || needs.changes.outputs.has_guidance == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Install Rust
uses: ./.github/actions/setup-rust
with:
components: rustfmt
- name: Install Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Check formatting
run: cargo fmt --all -- --check
- name: Validate h2 advisory exception
run: python3 scripts/ci/check_h2_advisory_exception.py
- name: Run cargo deny
uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
- name: Reject tracked files that match .gitignore
run: |
tracked_ignored="$(git ls-files -ci --exclude-standard)"
if [ -n "$tracked_ignored" ]; then
echo "Tracked files match .gitignore; remove them from source control or update .gitignore:"
printf '%s\n' "$tracked_ignored"
exit 1
fi
# Guards the #5603 Docker-outage class: every include_str! target must
# exist and be present in each Dockerfile build context.
- name: include_str! paths + Docker COPY coverage
run: scripts/ci/check-include-str-paths.sh
# The full checkout already contains the merge base needed by this
# diff-aware guard, so consolidating the jobs avoids another fetch.
- name: Hermetic env mutation guard
run: scripts/ci/check-hermetic-env.sh
- name: Static-check self-tests
run: |
scripts/ci/test-check-include-str-paths.sh
scripts/ci/test-check-hermetic-env.sh
scripts/ci/test-ci-comm-locale-pin.sh
scripts/ci/test-classify-test-scope.sh
scripts/ci/test-build-wasm-extensions.sh
scripts/ci/test-check-wasm-artifact-freshness.sh
scripts/ci/test-package-feature-flags.sh
scripts/ci/test-quality-gate-runner.sh
scripts/ci/test-ironclaw-integration-batch-runner.sh
scripts/ci/test-install-ci-apt-packages.sh
python3 scripts/ci/test_changed_workspace_packages.py
scripts/ci/test-reborn-crate-test-buckets.sh
scripts/ci/test-main-ci-slack-alerts.sh
python3 scripts/ci/test_ws12_suite_shards.py
python3 scripts/ci/test_ws12_workflow_contracts.py
python3 scripts/ci/test-check-target-tree.py
python3 scripts/ci/test-check-guidance.py
python3 scripts/test-check-type-duplicates.py
python3 scripts/ci/test_check_h2_advisory_exception.py
python3 scripts/ci/test_cut_ironclaw_release.py
scripts/ci/test-hermetic-test-process.sh
scripts/ci/test-reborn-docker-entrypoint.sh
# #7144: this 204-test module had never been run by any lane, so five
# of its assertions had silently drifted out of sync with the code they
# gate — including the live-canary shard roster, which pinned a
# `reborn-e2e.yml` string that no longer existed.
python3 -m unittest scripts.reborn_webui_v2_live_qa.test_run_live_qa
bash .github/scripts/test-pr-labeler.sh
- name: Self-test panic checker
if: github.event_name != 'push'
run: python3 scripts/check_no_panics.py --self-test
- name: Check Reborn production panic baseline
if: github.event_name != 'push'
run: python3 scripts/check_no_panics.py --reborn-baseline
- name: Check changed production code for panic-style calls
if: github.event_name != 'push'
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: python3 scripts/check_no_panics.py --base "$BASE_SHA" --head HEAD
# A committed `.wasm` whose `wasm-src/` moved on without it ships stale
# behaviour: the rebuild job overwrites the artifact in the working tree
# before testing it, so it never compares against what is committed.
- name: Check committed WASM artifacts are not stale
run: python3 scripts/ci/check-wasm-artifact-freshness.py
# Every other path gate answers "where is crate X?" by discovery, which
# is what lets them survive a family move — and is exactly why none of
# them can say the move landed where PROPOSAL §5 draws it. This one
# compares `cargo metadata` against the documented tree itself.
- name: Check the crate tree matches PROPOSAL §5
run: python3 scripts/ci/check-target-tree.py
# The guidance half of the same claim: every repo path the agent docs
# reference must be tracked, every `.claude/rules` `paths:` trigger must
# match at least one tracked file (a glob that matches nothing is a rule
# that never fires), and every crate must appear in its family's
# AGENTS.md crate table with a README.md (guidance-conventions.md).
- name: Check guidance references the tracked tree
run: python3 scripts/ci/check-guidance.py
- name: Check composition mass budget
run: bash scripts/ci/check-composition-budget.sh
- name: Self-test the composition budget gate
run: bash scripts/ci/test-check-composition-budget.sh
- name: Unit-test the dev-metrics tool
run: python3 scripts/test_dev_metrics.py
- name: Validate changed-coverage policy
run: >-
python3 scripts/ci/reborn_changed_coverage.py
--manifest tests/integration/changed-coverage-exemptions.toml
--validate-manifest-only
- name: Self-test exact release-binary smoke gate
run: python3 -m unittest tests/test_smoke_release_binary.py
webui-v2-js-lint:
name: WebUI v2 JS lint (no-undef)
needs: changes
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
# `working-directory:` is a YAML key, not a shell command — it cannot glob
# or resolve a path at runtime, so the family move (crates/<family>/
# ironclaw_webui, PROPOSAL §5) is resolved once here through the shared
# crate inventory (scripts/ci/crate-dir.sh -> scripts/ci/lib/crate_tree.py)
# rather than left as a literal every step below would need updating.
# `env` populated via $GITHUB_ENV is available to a later step's
# `working-directory:` (docs.github.com/actions: context availability for
# jobs.<job_id>.steps.working-directory includes `env`), so every
# subsequent step in THIS job can read it back through ${{ env.* }}.
- name: Resolve WebUI frontend directory
run: |
set -euo pipefail
webui_dir="$(bash scripts/ci/crate-dir.sh ironclaw_webui)"
echo "WEBUI_FRONTEND_DIR=${webui_dir}/frontend" >> "$GITHUB_ENV"
- name: Enable pnpm
run: corepack enable pnpm
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v4
with:
node-version: "22"
cache: "pnpm"
# actions/setup-node hashes this list to build the cache key
# (verified against the pinned commit's bundled dist/setup/index.js:
# `hashFiles` walks one globber built from every newline-separated
# pattern and only throws when the COMBINED walk finds nothing), so
# the exact line stays live for today's tree (WS7 put the crate in
# `crates/product/`) and the deeper glob picks up any later re-homing
# without either one needing to be conditional. The two must not
# overlap: `ws12_workflow_contracts.py` rejects a spare that already
# matches the real location, because a pattern matching everything is
# broad rather than depth-tolerant.
cache-dependency-path: |
crates/product/ironclaw_webui/frontend/pnpm-lock.yaml
crates/*/*/ironclaw_webui/frontend/pnpm-lock.yaml
# `no-undef` over WebUI v2 JavaScript catches production modules that reference a
# symbol they never imported. The VM-based component suites stub
# every collaborator through a `vm` context, so that class of bug (e.g. an
# htm `${Component}` used without an import) passes the unit tests and only
# surfaces at runtime — this is the gate the eval harness cannot provide.
- name: Install WebUI v2 frontend deps
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm install --frozen-lockfile
- name: Lint WebUI v2 JS
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm lint
# The full vitest suite (chat, extensions, telegram/slack panels, gate
# routing, channels tab, configure modal — the vm-tsx harness) previously
# never ran in CI: only the settings-scoped subset was wired elsewhere,
# so whole component suites could rot silently (the PR #5362 gap). The
# vm-tsx setup file is declared in vite.config.ts setupFiles; no extra
# CI configuration beyond node + pnpm is needed.
- name: WebUI v2 frontend tests (vitest)
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm test
# The SPA build is embedded into the serve binary at compile time through
# Cargo's OUT_DIR (see ironclaw_webui/build.rs), so a frontend that lints
# and tests but does not build still breaks downstream binary builds.
# Keep the build in the same job so the three gates travel together.
- name: Build WebUI v2 frontend
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm build
# The Storybook design-system catalog ships its own regression harness:
# the story suite runs in a headless Chromium (play functions, computed-
# style checks, and a11y=error), and the static build catches broken
# stories/config. `pnpm test` above deliberately runs only the browser-
# free `unit` project, so without these two steps the whole catalog could
# rot with CI still green (the `test:storybook` split gap).
# `hashFiles` takes an expression, so the lockfile path is built from the
# WEBUI_FRONTEND_DIR resolved above (crate-dir.sh) rather than spelled as
# a flat literal — `env` is available to `steps.<*>.with`, and
# ws12_workflow_contracts.py sanctions the flat literal only in the
# `cache-dependency-path` twinned-line shape.
- name: Cache Playwright browsers (Storybook suite)
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-storybook-${{ hashFiles(format('{0}/pnpm-lock.yaml', env.WEBUI_FRONTEND_DIR)) }}
restore-keys: ${{ runner.os }}-playwright-storybook-
- name: Install Playwright Chromium (Storybook suite)
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm exec playwright install --with-deps chromium
- name: WebUI v2 Storybook story tests (vitest, Chromium)
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm test:storybook
- name: Build WebUI v2 Storybook catalog
working-directory: ${{ env.WEBUI_FRONTEND_DIR }}
run: pnpm build-storybook
clippy:
name: Clippy (${{ matrix.name }})
needs: changes
if: >-
needs.changes.outputs.has_clippy == 'true' &&
(needs.changes.outputs.has_code == 'true' ||
needs.changes.outputs.clippy_scope == 'full')
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.changes.outputs.clippy_matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- name: Install Rust
uses: ./.github/actions/setup-rust
with:
components: clippy
- name: Enable pnpm for setup-node cache
if: contains(matrix.flags, '--all-features')
run: corepack enable pnpm
- name: Install Node.js for WebUI bundle builds
if: contains(matrix.flags, '--all-features')
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v4
with:
node-version: "22"
cache: "pnpm"
# See the webui-v2-js-lint job above for why this is a depth-tolerant
# glob list rather than a single literal.
cache-dependency-path: |
crates/product/ironclaw_webui/frontend/pnpm-lock.yaml
crates/*/*/ironclaw_webui/frontend/pnpm-lock.yaml
- name: Enable pnpm
if: contains(matrix.flags, '--all-features')
run: corepack enable pnpm
- name: Install WebUI frontend dependencies
if: contains(matrix.flags, '--all-features')
run: |
set -euo pipefail
webui_dir="$(bash scripts/ci/crate-dir.sh ironclaw_webui)"
cd "${webui_dir}/frontend"
pnpm install --frozen-lockfile
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
shared-key: clippy
# Keep saves to protected-branch and merge_group runs so the ~10 GB
# repo cache LRU is seeded by shared states, not arbitrary PR branches.
save-if: ${{ matrix.name == 'all-features' && ((github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'merge_group') }}
- name: Setup OVH sccache
uses: ./.github/actions/setup-sccache-dist
with:
scheduler-url: ${{ vars.SCCACHE_DIST_SCHEDULER_URL }}
auth-token: ${{ secrets.SCCACHE_DIST_AUTH_TOKEN }}
cache-ssh-host: ${{ vars.SCCACHE_CACHE_SSH_HOST }}
cache-ssh-user: ${{ vars.SCCACHE_CACHE_SSH_USER }}
cache-ssh-port: ${{ vars.SCCACHE_CACHE_SSH_PORT }}
cache-ssh-private-key: ${{ secrets.SCCACHE_CACHE_SSH_PRIVATE_KEY }}
cache-ssh-known-hosts: ${{ secrets.SCCACHE_CACHE_SSH_KNOWN_HOSTS }}
redis-password: ${{ secrets.SCCACHE_REDIS_PASSWORD }}
- name: Check production-target lints
if: github.event_name == 'pull_request' && needs.changes.outputs.clippy_scope == 'selected'
env:
CLIPPY_PACKAGES: ${{ needs.changes.outputs.clippy_packages }}
run: |
package_args=()
while IFS= read -r package; do
package_args+=(-p "${package}")
done < <(jq -r '.[]' <<< "${CLIPPY_PACKAGES}")
# No explicit target filter. `--lib` is a hard error on a bin-only
# package ("no library targets found in package `ironclaw`"), which
# is exactly what a PR touching only crates/app/ironclaw_cli
# selects, and `--bins` alone degrades to "no targets matched; this
# is a no-op" on a lib-only package — the lane would report green
# having linted nothing. Cargo's default target set for a package is
# already lib + bins, with tests, examples, and benches excluded, so
# omitting both keeps this the production-target lane for every
# package shape. scripts/ci/ws12_workflow_contracts.py pins this.
cargo clippy "${package_args[@]}" \
${{ matrix.flags }} -- -D warnings
- name: Check affected all-target lints
if: github.event_name == 'merge_group' && needs.changes.outputs.clippy_scope == 'selected'
env:
CLIPPY_PACKAGES: ${{ needs.changes.outputs.clippy_packages }}
run: |
package_args=()
while IFS= read -r package; do
package_args+=(-p "${package}")
done < <(jq -r '.[]' <<< "${CLIPPY_PACKAGES}")
cargo clippy "${package_args[@]}" --all-targets \
${{ matrix.flags }} -- -D warnings
- name: Check affected production-target lints (no dev-dependency features)
if: github.event_name == 'merge_group' && needs.changes.outputs.clippy_scope == 'selected' && matrix.name == 'default'
env:
CLIPPY_PACKAGES: ${{ needs.changes.outputs.clippy_packages }}
run: |
package_args=()
while IFS= read -r package; do
package_args+=(-p "${package}")
done < <(jq -r '.[]' <<< "${CLIPPY_PACKAGES}")
cargo clippy "${package_args[@]}" -- -D warnings
- name: Check all-target lints
if: needs.changes.outputs.clippy_scope == 'full'
run: cargo clippy --all --all-targets ${{ matrix.flags }} -- -D warnings
# #7119. The PR lane above lints only the *changed* packages, so a crate
# that is merely a dependency of that set compiles with whatever features
# the set happens to enable. `--lib --bins` builds no dev-dependencies, and
# in this workspace `test-support` is turned on exclusively through
# dev-dependency edges — so it is OFF here and ON in every `--tests` lane.
# Code behind `cfg(any(test, feature = "test-support"))` then vanishes and
# a `use` that only it names becomes an unused import, which no
# whole-workspace lane could see: `--all --tests` builds the dev-deps that
# unify the feature back on. This step is that missing shape, workspace-
# wide, so the failure lands at merge time instead of waiting for the one
# PR whose diff happens to produce the tripping package set.
#
# Deliberately the `default` flavor: `--all-features` re-enables
# `test-support` on every selected package and masks the exact class this
# step exists to catch.
- name: Check production-target lints (workspace, no dev-dependency features)
if: needs.changes.outputs.clippy_scope == 'full' && matrix.name == 'default'
run: cargo clippy --all --lib --bins -- -D warnings
clippy-windows:
name: Clippy Windows (${{ matrix.name }})
needs: changes
if: needs.changes.outputs.has_code == 'true' && github.event_name == 'push'
runs-on: windows-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.changes.outputs.clippy_matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- name: Install Rust
uses: ./.github/actions/setup-rust
with:
components: clippy
- name: Enable pnpm for setup-node cache
if: contains(matrix.flags, '--all-features')
run: corepack enable pnpm
- name: Install Node.js for WebUI bundle builds
if: contains(matrix.flags, '--all-features')
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v4
with:
node-version: "22"
cache: "pnpm"
# See the webui-v2-js-lint job above for why this is a depth-tolerant
# glob list rather than a single literal.
cache-dependency-path: |
crates/product/ironclaw_webui/frontend/pnpm-lock.yaml
crates/*/*/ironclaw_webui/frontend/pnpm-lock.yaml
- name: Enable pnpm
if: contains(matrix.flags, '--all-features')
run: corepack enable pnpm
- name: Install WebUI frontend dependencies
if: contains(matrix.flags, '--all-features')
shell: bash
run: |
set -euo pipefail
webui_dir="$(bash scripts/ci/crate-dir.sh ironclaw_webui)"
cd "${webui_dir}/frontend"
pnpm install --frozen-lockfile
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
key: windows-${{ matrix.name }}
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Check lints
run: cargo clippy --all --tests --examples ${{ matrix.flags }} -- -D warnings
reborn-cli-smoke:
name: Reborn CLI smoke tests
needs: changes
if: needs.changes.outputs.has_reborn_cli == 'true' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- name: Install Rust
uses: ./.github/actions/setup-rust
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
shared-key: reborn-cli-smoke
# Keep saves to protected-branch and merge_group runs so the ~10 GB
# repo cache LRU is seeded by shared states, not arbitrary PR branches.
save-if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'merge_group' }}
- name: Setup OVH sccache
uses: ./.github/actions/setup-sccache-dist
with:
# Keep this Wasmtime-heavy smoke target cache-only. sccache-dist can
# corrupt generated Wasmtime/Wiggle inputs and produce bogus type
# resolution failures.
cache-ssh-host: ${{ vars.SCCACHE_CACHE_SSH_HOST }}
cache-ssh-user: ${{ vars.SCCACHE_CACHE_SSH_USER }}
cache-ssh-port: ${{ vars.SCCACHE_CACHE_SSH_PORT }}
cache-ssh-private-key: ${{ secrets.SCCACHE_CACHE_SSH_PRIVATE_KEY }}
cache-ssh-known-hosts: ${{ secrets.SCCACHE_CACHE_SSH_KNOWN_HOSTS }}
redis-password: ${{ secrets.SCCACHE_REDIS_PASSWORD }}
- name: Test Reborn boot config crate
run: cargo test -p ironclaw_config
- name: Test Reborn CLI binary crate
run: cargo test -p ironclaw
- name: Test Reborn libSQL restart integration
run: |
cargo test -p ironclaw_host_runtime \
--test reborn_durable_restart_integration \
-- --nocapture
- name: Test Reborn architecture boundaries
run: cargo test -p ironclaw_architecture_tests reborn
# docs/ mixes the public Mintlify site with internal engineering docs.
# Omission from docs.json navigation is NOT a publication boundary — a page
# left out of navigation is still deployed, reachable by URL, and indexable.
# This gate fails when a .md/.mdx under docs/ is neither in navigation nor
# fenced by docs/.mintignore (nor explicitly marked `hidden: true`), when
# navigation references a page with no source file, and when .mintignore
# grows a new entry — the fence list is frozen; new internal docs belong
# under docs/internal/.
docs-publication-boundary:
name: Docs publication boundary
needs: changes
if: needs.changes.outputs.has_docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- name: Install Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Self-test the publication boundary check
run: python3 scripts/ci/test_docs_publication_boundary.py
- name: Check every docs/ page is published or fenced
run: python3 scripts/ci/docs_publication_boundary.py
code-style:
name: Code Style (fmt + clippy)
runs-on: ubuntu-latest
timeout-minutes: 15
if: always()
needs:
- changes
- fast-checks
- clippy
- clippy-windows
- reborn-cli-smoke
- webui-v2-js-lint
- docs-publication-boundary
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
- run: |
# A docs-only PR has has_code=false, so the boundary gate must be
# checked before the early exit below or it could never block.
if [[ "${{ needs.changes.outputs.has_docs }}" == "true" && "${{ needs.docs-publication-boundary.result }}" != "success" ]]; then
echo "Docs publication boundary failed: ${{ needs.docs-publication-boundary.result }}"
exit 1
fi
# Same reason as the docs gate above: a guidance-only PR has
# has_code=false, so fast-checks (which carries check-guidance.py)
# must be judged before the early exit or the gate would run and
# never be able to block — the inert-guard shape this whole change
# exists to remove.
if [[ "${{ needs.changes.outputs.has_guidance }}" == "true" && "${{ needs.fast-checks.result }}" != "success" ]]; then
echo "fast-checks failed: ${{ needs.fast-checks.result }}"
exit 1
fi
if [[ "${{ needs.changes.outputs.has_code }}" == "false" &&
"${{ needs.changes.outputs.clippy_scope }}" != "full" ]]; then
if [[ "${{ needs.changes.outputs.has_reborn_cli }}" == "true" && "${{ needs.reborn-cli-smoke.result }}" != "success" ]]; then
echo "Reborn CLI smoke failed: ${{ needs.reborn-cli-smoke.result }}"
exit 1
fi
echo "No code changes — style checks skipped correctly"
exit 0
fi
# shellcheck disable=SC1091
if ! source .github/scripts/ci-job-result-ok.sh; then
echo "Unable to source .github/scripts/ci-job-result-ok.sh"
exit 1
fi
for job_result in \
"fast-checks=${{ needs.fast-checks.result }}" \
"webui-v2-js-lint=${{ needs.webui-v2-js-lint.result }}"; do
name="${job_result%%=*}"
result="${job_result##*=}"
if ! job_result_ok "$name" "$result" false "allow"; then
echo "$name failed: $result"
exit 1
fi
done
clippy_may_skip=false
if [[ "${{ needs.changes.outputs.has_clippy }}" != "true" ]]; then
clippy_may_skip=true
fi
if ! job_result_ok \
"clippy" "${{ needs.clippy.result }}" "${clippy_may_skip}" "allow"; then
echo "clippy failed: ${{ needs.clippy.result }}"
exit 1
fi
for job_result in \
"reborn-cli-smoke=${{ needs.reborn-cli-smoke.result }}" \
"clippy-windows=${{ needs.clippy-windows.result }}"; do
name="${job_result%%=*}"
result="${job_result##*=}"
if ! job_result_ok "$name" "$result" true "allow"; then
echo "$name failed: $result"
exit 1
fi
done
- name: Assert the full clippy matrix ran (merge queue + push)
if: needs.changes.outputs.clippy_scope == 'full' && github.event_name != 'pull_request'
env:
MATRIX_JSON: ${{ needs.changes.outputs.clippy_matrix }}
run: |
# This roll-up is a required check. A green result must mean the full
# feature matrix was linted, so fail loudly if the matrix config ever
# regresses to the slim PR lane on a merge-queue or push run.
for lane in all-features default; do
if ! printf '%s' "$MATRIX_JSON" | jq -e --arg lane "$lane" 'any(.[]; .name == $lane)' > /dev/null; then
echo "Required clippy lane missing from matrix on ${{ github.event_name }}: $lane"
echo "Matrix was: $MATRIX_JSON"
exit 1
fi
done