mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
The gateway's static frontend had grown into two merge-conflict hotspots: a
6 887-line `style.css` and an 11 189-line `app.js`, each a catch-all for every
surface of the SPA. Any two PRs touching different tabs were likely to collide.
This change splits both files by surface/concern while preserving bytes and
behavior. `STYLE_CSS` and `APP_JS` in `crates/ironclaw_gateway/src/assets.rs`
now `concat!(include_str!(...))` the split pieces at compile time, so the
served `/style.css` and `/app.js` URLs are unchanged and the existing
workspace-overlay (`custom.css`) path still works. Cuts land on function /
block boundaries; `node --check` validates the concat. Admin assets move
under `static/admin/` for symmetry. Per-commit safety + CI workflow validate
the split files per-file instead of the old monolith.
- Styles split into 20 files under `static/styles/{base,layout}.css +
styles/{components,primitives,surfaces}/*.css`
- JS split into 24 files under `static/js/{core,surfaces}/*.js`
- No URL, CSP, or behavioural change — pure file-layout refactor
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
304 lines
12 KiB
YAML
304 lines
12 KiB
YAML
name: Code Style
|
|
on:
|
|
pull_request:
|
|
# Pushes to main/staging refresh the rust-cache entries that PR jobs
|
|
# restore from. PR jobs themselves are restore-only (see `save-if`
|
|
# on the rust-cache steps below).
|
|
push:
|
|
branches:
|
|
- main
|
|
- staging
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: code-style-${{ github.head_ref || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# ── Skip when only docs/config changed on a PR ──
|
|
changes:
|
|
name: Detect code changes
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
has_code: ${{ steps.non_pr.outputs.has_code || steps.pr_check.outputs.has_code }}
|
|
# `has_boundary_check` fires when the gateway-boundaries guardrail
|
|
# itself changes (either the checker script or this workflow). Keeps
|
|
# the check from being silently skipped by a PR that only edits
|
|
# scripts/.github — see serrrfirat's review on PR #2647.
|
|
has_boundary_check: ${{ steps.non_pr.outputs.has_code || steps.pr_check.outputs.has_boundary_check }}
|
|
steps:
|
|
- id: non_pr
|
|
if: github.event_name != 'pull_request'
|
|
run: echo "has_code=true" >> "$GITHUB_OUTPUT"
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
if: github.event_name == 'pull_request'
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
- id: pr_check
|
|
if: github.event_name == 'pull_request'
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
run: |
|
|
CODE_CHANGES=$(git diff --name-only "$BASE_SHA"...HEAD -- \
|
|
'src/' 'crates/' 'channels-src/' 'tools-src/' 'tests/' 'migrations/' \
|
|
'Cargo.toml' 'Cargo.lock' 'Dockerfile' 'build.rs' \
|
|
| head -1)
|
|
if [ -n "$CODE_CHANGES" ]; then
|
|
echo "has_code=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "has_code=false" >> "$GITHUB_OUTPUT"
|
|
echo "No code changes — style checks will be skipped"
|
|
fi
|
|
|
|
BOUNDARY_CHANGES=$(git diff --name-only "$BASE_SHA"...HEAD -- \
|
|
'scripts/check_gateway_boundaries.py' \
|
|
'.github/workflows/code_style.yml' \
|
|
| head -1)
|
|
if [ -n "$BOUNDARY_CHANGES" ]; then
|
|
echo "has_boundary_check=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "has_boundary_check=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# ── Dynamic matrix: SLIM (1 leg) for non-main PRs; FULL (3 legs) for PRs
|
|
# to main and pushes to long-lived branches.
|
|
clippy-matrix:
|
|
name: Configure clippy matrix
|
|
needs: changes
|
|
if: needs.changes.outputs.has_code == 'true'
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.set.outputs.matrix }}
|
|
steps:
|
|
- id: set
|
|
run: |
|
|
FULL='[{"name":"all-features","flags":"--all-features"},{"name":"default","flags":""},{"name":"libsql-only","flags":"--no-default-features --features libsql"}]'
|
|
SLIM='[{"name":"all-features","flags":"--all-features"}]'
|
|
|
|
# Full matrix on push (cache-warming + verification across configs)
|
|
# and on PRs targeting main (final promotion gate). Other PRs use
|
|
# SLIM since lint findings are almost never feature-gated.
|
|
if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.base_ref }}" = "main" ]; then
|
|
echo "matrix=${FULL}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "matrix=${SLIM}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
format:
|
|
name: Formatting
|
|
needs: changes
|
|
if: needs.changes.outputs.has_code == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
|
with:
|
|
components: rustfmt
|
|
- name: Check formatting
|
|
run: cargo fmt --all -- --check
|
|
|
|
gateway-js-syntax:
|
|
name: Gateway JS syntax
|
|
needs: changes
|
|
if: needs.changes.outputs.has_code == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- name: Install Node.js
|
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
|
with:
|
|
node-version: "22"
|
|
- name: Check gateway JS syntax
|
|
run: |
|
|
# app.js was split into per-surface/per-concern modules under
|
|
# static/js/ that are concatenated at compile time into APP_JS.
|
|
# Cuts land on top-level symbol boundaries, so each file is
|
|
# self-parseable — a per-file node --check is sufficient.
|
|
find crates/ironclaw_gateway/static/js -type f -name '*.js' \
|
|
-exec node --check {} +
|
|
|
|
deny-check:
|
|
name: cargo-deny
|
|
needs: changes
|
|
if: needs.changes.outputs.has_code == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- name: Run cargo deny
|
|
uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
|
|
|
|
clippy:
|
|
name: Clippy (${{ matrix.name }})
|
|
needs: [changes, clippy-matrix]
|
|
if: needs.changes.outputs.has_code == 'true'
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include: ${{ fromJSON(needs.clippy-matrix.outputs.matrix) }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
|
with:
|
|
components: clippy
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
# Single shared slot across the clippy matrix. The all-features leg
|
|
# builds a superset of the other configs; subset legs (push only)
|
|
# restore from it. Only all-features writes the slot, and only on
|
|
# main/staging pushes — preventing a subset leg from winning the
|
|
# cache race and pinning the slot to a partial build.
|
|
shared-key: clippy
|
|
save-if: ${{ matrix.name == 'all-features' && github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
|
|
# `--benches` is intentionally omitted: lints rarely diverge in bench code,
|
|
# and pulling in `criterion` adds ~30s+ of dependency compilation per run.
|
|
# `bench-compile` in test.yml provides the type-check signal for benches.
|
|
- name: Check lints
|
|
run: cargo clippy --all --tests --examples ${{ matrix.flags }} -- -D warnings
|
|
|
|
clippy-windows:
|
|
name: Clippy Windows (${{ matrix.name }})
|
|
needs: [changes, clippy-matrix]
|
|
# PR-to-main runs the full Windows matrix; pushes to long-lived branches
|
|
# run it for cache warming. Other PRs skip Windows lint — windows-build
|
|
# in test.yml provides the Windows compile signal.
|
|
if: >
|
|
needs.changes.outputs.has_code == 'true' &&
|
|
(github.event_name == 'push' || github.base_ref == 'main')
|
|
runs-on: windows-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include: ${{ fromJSON(needs.clippy-matrix.outputs.matrix) }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- name: Install Rust
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
|
with:
|
|
components: clippy
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
# Per-variant Windows cache, shared with test.yml's windows-build
|
|
# job. windows-build runs on push (where save-if allows writes), so
|
|
# clippy-windows restores from a warm cache on PRs.
|
|
key: windows-${{ matrix.name }}
|
|
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
|
|
# `--benches` omitted; see comment on the Linux clippy job above.
|
|
- name: Check lints
|
|
run: cargo clippy --all --tests --examples ${{ matrix.flags }} -- -D warnings
|
|
|
|
no-panics:
|
|
name: No panics in production code
|
|
needs: changes
|
|
# Diff is computed against the PR base SHA, which is unavailable on push.
|
|
# Gate to PRs only; the roll-up tolerates a skipped result on push.
|
|
if: github.event_name == 'pull_request' && needs.changes.outputs.has_code == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
with:
|
|
python-version: "3.12"
|
|
- name: Check for .unwrap(), .expect(), assert!() in production code
|
|
run: |
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
python3 scripts/check_no_panics.py --base "$BASE" --head HEAD
|
|
|
|
gateway-boundaries:
|
|
name: Gateway platform/feature boundaries
|
|
needs: changes
|
|
# Also run when the checker script or this workflow itself is edited —
|
|
# otherwise a PR touching only `scripts/check_gateway_boundaries.py`
|
|
# or `.github/workflows/code_style.yml` would skip this job and still
|
|
# satisfy the roll-up, weakening the guardrail. See serrrfirat's
|
|
# review on PR #2647.
|
|
if: needs.changes.outputs.has_code == 'true' || needs.changes.outputs.has_boundary_check == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
with:
|
|
python-version: "3.12"
|
|
- name: Check that platform/ has no back-edges into handlers/ or features/
|
|
run: python3 scripts/check_gateway_boundaries.py
|
|
- name: Self-test the boundary script
|
|
run: python3 scripts/check_gateway_boundaries.py test
|
|
|
|
# Roll-up job for branch protection
|
|
code-style:
|
|
name: Code Style (fmt + gateway-js-syntax + clippy + deny)
|
|
runs-on: ubuntu-latest
|
|
if: always()
|
|
needs:
|
|
- changes
|
|
- clippy-matrix
|
|
- format
|
|
- gateway-js-syntax
|
|
- clippy
|
|
- clippy-windows
|
|
- deny-check
|
|
- no-panics
|
|
- gateway-boundaries
|
|
steps:
|
|
- run: |
|
|
# Docs-only PRs intentionally skip every gated job — that's a pass.
|
|
if [[ "${{ needs.changes.outputs.has_code }}" == "false" ]]; then
|
|
echo "No code changes — style checks skipped correctly"
|
|
exit 0
|
|
fi
|
|
|
|
# Always-required jobs.
|
|
for job_result in \
|
|
"format=${{ needs.format.result }}" \
|
|
"gateway-js-syntax=${{ needs.gateway-js-syntax.result }}" \
|
|
"clippy=${{ needs.clippy.result }}" \
|
|
"deny-check=${{ needs.deny-check.result }}" \
|
|
"gateway-boundaries=${{ needs.gateway-boundaries.result }}"; do
|
|
name="${job_result%%=*}"
|
|
result="${job_result##*=}"
|
|
if [[ "$result" != "success" ]]; then
|
|
echo "$name failed: $result"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Conditional jobs: must succeed when run, may be skipped on
|
|
# events where their `if:` filter excludes them.
|
|
for job_result in \
|
|
"no-panics=${{ needs.no-panics.result }}" \
|
|
"clippy-windows=${{ needs.clippy-windows.result }}"; do
|
|
name="${job_result%%=*}"
|
|
result="${job_result##*=}"
|
|
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
|
|
echo "$name failed: $result"
|
|
exit 1
|
|
fi
|
|
done
|