Files
ironclaw/.github/workflows/code_style.yml
Illia Polosukhin a35f9d9ab5 refactor(gateway): split monolithic style.css and app.js into per-surface modules (#2683)
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>
2026-04-20 00:12:29 +09:00

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