Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 105 additions & 148 deletions .github/workflows/code_style.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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).
merge_group:
branches:
- main
types:
- checks_requested
# Pushes to main/staging refresh the rust-cache entries that PR and
# merge-group jobs restore from. Non-push runs stay restore-only.
push:
branches:
- main
Expand All @@ -17,54 +21,43 @@ concurrency:
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 }}
has_code: ${{ steps.non_pr.outputs.has_code || steps.diff.outputs.has_code }}
has_boundary_check: ${{ steps.non_pr.outputs.has_boundary_check || steps.diff.outputs.has_boundary_check }}
steps:
- id: non_pr
if: github.event_name != 'pull_request'
run: echo "has_code=true" >> "$GITHUB_OUTPUT"
if: github.event_name == 'push'
run: |
echo "has_code=true" >> "$GITHUB_OUTPUT"
echo "has_boundary_check=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
with:
fetch-depth: 0
persist-credentials: false
- id: pr_check
if: github.event_name == 'pull_request'
- id: diff
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.merge_group.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
CHANGED_FILES="$(git diff --name-only "$BASE_SHA"...HEAD)"

if printf '%s\n' "$CHANGED_FILES" | grep -Eq '^(src/|crates/|channels-src/|tools-src/|tests/|migrations/|Cargo\.toml$|Cargo\.lock$|Dockerfile$|build\.rs$|scripts/check_no_panics\.py$|scripts/check_gateway_boundaries\.py$|\.github/workflows/code_style\.yml$)'; 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
if printf '%s\n' "$CHANGED_FILES" | grep -Eq '^(scripts/check_gateway_boundaries\.py$|\.github/workflows/code_style\.yml$)'; 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
Expand All @@ -78,13 +71,10 @@ jobs:
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
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "matrix=${SLIM}" >> "$GITHUB_OUTPUT"
else
echo "matrix=${FULL}" >> "$GITHUB_OUTPUT"
fi

format:
Expand All @@ -93,52 +83,48 @@ jobs:
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
- 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 {} +
- 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: |
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
- 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 }})
Expand All @@ -150,108 +136,83 @@ jobs:
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
- 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:
shared-key: clippy
save-if: ${{ matrix.name == 'all-features' && github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
- 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')
if: needs.changes.outputs.has_code == 'true' && github.event_name != 'pull_request'
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
- 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:
key: windows-${{ matrix.name }}
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
- 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'
if: needs.changes.outputs.has_code == 'true' && github.event_name != 'push'
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
- 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
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

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
- 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
Expand All @@ -268,13 +229,11 @@ jobs:
- 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 }}" \
Expand All @@ -289,8 +248,6 @@ jobs:
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
Expand Down
Loading
Loading