mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-02 23:56:24 +08:00
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>
735 lines
37 KiB
YAML
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
|