ci: speed up CI feedback loop (#2566)

* ci: speed up feedback loop — concurrency, dynamic matrix, path skip, faster staging

- Add cancel-in-progress concurrency groups to 6 workflows (test, code_style,
  e2e, regression-test-check, pr-label-classify, pr-label-scope) so pushes
  to the same branch cancel stale CI runs instead of queuing behind them.

- Collapse test/clippy matrix on PRs from 3 configs to 1 (all-features).
  Full 3-config matrix still runs on staging promotion and push-to-main.
  Cuts PR compilation from ~3x to ~1x.

- Reduce staging-ci poll interval from 60 minutes to 10 minutes, cutting
  worst-case promotion latency by 6x.

- Add path-based skip to test.yml and code_style.yml: a lightweight
  changes-detection job checks if any code files changed (src/, crates/,
  Cargo.*, etc.). Docs-only PRs skip all Rust compilation while the
  rollup job still passes for branch protection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: collapse nested ifs in trace_contains_tool_call match arms

Clippy 1.95 added/tightened `clippy::collapsible_match`. The two
nested `if`s in this helper are equivalent to additional match-arm
guards, which is what the lint suggests. No behavior change.

Inherited from #2268's merge into staging; would have failed
`Clippy (all-features)` on every PR until fixed.

* test: rustfmt struct destructure in collapsed match arm

* ci: drop --benches from clippy invocations

`--benches` pulls in `criterion` (heavy dep) but only covers 2 bench
files in `crates/ironclaw_safety/`. Lints rarely differ in bench code,
and `bench-compile` in test.yml already provides the type-check signal.

Cold-cache impact: ~30s+ saved per Linux/Windows leg (criterion +
plotters + ciborium chain). Warm-cache: marginal but non-zero.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: ilblackdragon@gmail.com <ilblackdragon@gmail.com>
This commit is contained in:
firat.sertgoz
2026-04-18 10:46:03 +03:00
committed by GitHub
parent 7f5b02d7f0
commit c74f9555da
13 changed files with 261 additions and 90 deletions

View File

@@ -0,0 +1,18 @@
name: Install cargo-component
description: Install cargo-component using a precompiled binary (taiki-e/install-action).
# Replaces ad-hoc `cargo install cargo-component --locked || true` calls. The
# precompiled-binary path takes a few seconds vs. several minutes for a source
# install, and we no longer swallow install failures.
#
# The version is pinned for build reproducibility — same rationale as
# `--locked` for source installs. Bump alongside the cargo-component dependency
# in dependabot's `cargo-component*` group (see .github/dependabot.yml).
runs:
using: composite
steps:
- name: Install cargo-component
uses: taiki-e/install-action@62b0f2dec647a8e604c6a0fda0e38530180dce20 # v2
with:
tool: cargo-component@0.21.1

View File

@@ -12,9 +12,70 @@ on:
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 }}
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
# ── 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
@@ -30,6 +91,8 @@ jobs:
gateway-js-syntax:
name: Gateway JS syntax
needs: changes
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -45,6 +108,8 @@ jobs:
deny-check:
name: cargo-deny
needs: changes
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -56,22 +121,13 @@ jobs:
clippy:
name: Clippy (${{ matrix.name }})
# Push events only run the all-features leg. That leg builds a superset
# of artifacts, so it deterministically wins the shared cache slot on
# main/staging refreshes and PR legs always restore from a useful
# starting point. PRs still run all three to enforce lint coverage.
if: github.event_name == 'pull_request' || matrix.name == 'all-features'
needs: [changes, clippy-matrix]
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: all-features
flags: "--all-features"
- name: default
flags: ""
- name: libsql-only
flags: "--no-default-features --features libsql"
include: ${{ fromJSON(needs.clippy-matrix.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -83,29 +139,33 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Share a single cache across the clippy matrix. `target/` built for
# --all-features is a superset of the other two legs, so restoring
# from whichever variant saved last is still faster than a cold build
# and avoids storing three near-duplicate copies in the GHA cache.
# 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: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
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 --benches --tests --examples ${{ matrix.flags }} -- -D warnings
run: cargo clippy --all --tests --examples ${{ matrix.flags }} -- -D warnings
clippy-windows:
name: Clippy Windows (${{ matrix.name }})
if: github.base_ref == 'main'
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:
- name: all-features
flags: "--all-features"
- name: default
flags: ""
- name: libsql-only
flags: "--no-default-features --features libsql"
include: ${{ fromJSON(needs.clippy-matrix.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -117,19 +177,21 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Share the per-variant Windows target cache with test.yml's
# windows-build job. That job runs on `push` to main (where save-if
# allows writes), so clippy-windows — which only runs on main PRs
# can restore from a warm cache instead of cold-building every time.
# 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 --benches --tests --examples ${{ matrix.flags }} -- -D warnings
run: cargo clippy --all --tests --examples ${{ matrix.flags }} -- -D warnings
no-panics:
name: No panics in production code
# Compares the PR's changes against the PR base; not meaningful on push.
if: github.event_name == 'pull_request'
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
@@ -150,20 +212,46 @@ jobs:
name: Code Style (fmt + gateway-js-syntax + clippy + deny)
runs-on: ubuntu-latest
if: always()
needs: [format, gateway-js-syntax, clippy, clippy-windows, deny-check, no-panics]
needs:
- changes
- clippy-matrix
- format
- gateway-js-syntax
- clippy
- clippy-windows
- deny-check
- no-panics
steps:
- run: |
if [[ "${{ needs.format.result }}" != "success" || "${{ needs.gateway-js-syntax.result }}" != "success" || "${{ needs.clippy.result }}" != "success" || "${{ needs.deny-check.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
# no-panics only runs on pull_request events, so skipped is acceptable on push but failure is not
if [[ "${{ needs.no-panics.result }}" != "success" && "${{ needs.no-panics.result }}" != "skipped" ]]; then
echo "no-panics failed: ${{ needs.no-panics.result }}"
exit 1
fi
# clippy-windows only runs on main PRs, so skipped is acceptable but failure is not
if [[ "${{ needs.clippy-windows.result }}" != "success" && "${{ needs.clippy-windows.result }}" != "skipped" ]]; then
echo "Windows clippy failed: ${{ needs.clippy-windows.result }}"
exit 1
# 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 }}"; 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

View File

@@ -86,10 +86,7 @@ jobs:
uses: taiki-e/install-action@62b0f2dec647a8e604c6a0fda0e38530180dce20 # cargo-llvm-cov
- name: Install cargo-component
run: |
if ! command -v cargo-component >/dev/null 2>&1; then
cargo install cargo-component --locked
fi
uses: ./.github/actions/install-cargo-component
- name: Build WASM channels (for integration tests)
run: ./scripts/build-wasm-extensions.sh --channels
@@ -150,10 +147,7 @@ jobs:
uses: taiki-e/install-action@62b0f2dec647a8e604c6a0fda0e38530180dce20 # cargo-llvm-cov
- name: Install cargo-component
run: |
if ! command -v cargo-component >/dev/null 2>&1; then
cargo install cargo-component --locked
fi
uses: ./.github/actions/install-cargo-component
- name: Build WASM channels
run: ./scripts/build-wasm-extensions.sh --channels

View File

@@ -19,6 +19,10 @@ on:
permissions:
contents: read
concurrency:
group: e2e-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
# ── Step 1: compile once ──────────────────────────────────────────────────
build:

View File

@@ -9,6 +9,10 @@ permissions:
pull-requests: write
issues: read # needed for search/issues API (contributor count)
concurrency:
group: pr-classify-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
classify:
runs-on: ubuntu-latest

View File

@@ -9,6 +9,10 @@ permissions:
issues: write
pull-requests: write
concurrency:
group: pr-scope-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
scope:
runs-on: ubuntu-latest

View File

@@ -6,6 +6,10 @@ on:
permissions:
contents: read
concurrency:
group: regression-check-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
regression-test:
name: Regression test enforcement

View File

@@ -279,9 +279,9 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain + wasm target
run: |
rustup target add wasm32-wasip2
cargo install cargo-component --locked || true
run: rustup target add wasm32-wasip2
- name: Install cargo-component
uses: ./.github/actions/install-cargo-component
- uses: swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: wasm-extensions

View File

@@ -2,7 +2,7 @@ name: Staging CI (Batched)
on:
schedule:
- cron: "0 * * * *" # Every 60 minutes
- cron: "*/10 * * * *" # Every 10 minutes (down from 60)
workflow_dispatch:
inputs:
force:

View File

@@ -16,24 +16,73 @@ on:
permissions:
contents: read
concurrency:
group: test-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
# ── Skip expensive jobs 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 }}
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 detected — expensive jobs will be skipped"
fi
# ── Dynamic matrix: PRs run only all-features; full matrix on staging/push ──
matrix-config:
name: Configure matrix
runs-on: ubuntu-latest
outputs:
test_matrix: ${{ steps.set.outputs.test_matrix }}
windows_matrix: ${{ steps.set.outputs.windows_matrix }}
steps:
- id: set
run: |
FULL='[{"name":"all-features","flags":"--no-default-features --features postgres,libsql,html-to-markdown,bedrock,import"},{"name":"default","flags":""},{"name":"libsql-only","flags":"--no-default-features --features libsql"}]'
SLIM='[{"name":"all-features","flags":"--no-default-features --features postgres,libsql,html-to-markdown,bedrock,import"}]'
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "test_matrix=${SLIM}" >> "$GITHUB_OUTPUT"
echo "windows_matrix=${SLIM}" >> "$GITHUB_OUTPUT"
else
echo "test_matrix=${FULL}" >> "$GITHUB_OUTPUT"
echo "windows_matrix=${FULL}" >> "$GITHUB_OUTPUT"
fi
tests:
name: Tests (${{ matrix.name }})
needs: [changes, matrix-config]
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- name: all-features
# Keep product feature coverage broad without pulling in the
# test-only `integration` feature, which is exercised separately
# in the heavy integration job below.
flags: "--no-default-features --features postgres,libsql,html-to-markdown,bedrock,import"
- name: default
flags: ""
- name: libsql-only
flags: "--no-default-features --features libsql"
include: ${{ fromJSON(needs.matrix-config.outputs.test_matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -49,7 +98,7 @@ jobs:
key: ${{ matrix.name }}
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
- name: Install cargo-component
run: cargo install cargo-component --locked || true
uses: ./.github/actions/install-cargo-component
- name: Build WASM channels (for integration tests)
run: ./scripts/build-wasm-extensions.sh --channels
- name: Run Tests
@@ -59,6 +108,8 @@ jobs:
heavy-integration-tests:
name: Heavy Integration Tests
needs: changes
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
@@ -111,6 +162,7 @@ jobs:
windows-build:
name: Windows Build (${{ matrix.name }})
needs: matrix-config
if: >
github.event_name != 'pull_request' ||
github.base_ref != 'staging'
@@ -118,13 +170,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- name: all-features
flags: "--no-default-features --features postgres,libsql,html-to-markdown,bedrock,import"
- name: default
flags: ""
- name: libsql-only
flags: "--no-default-features --features libsql"
include: ${{ fromJSON(needs.matrix-config.outputs.windows_matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -162,7 +208,7 @@ jobs:
key: wasm-extensions
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
- name: Install cargo-component
run: cargo install cargo-component --locked || true
uses: ./.github/actions/install-cargo-component
- name: Build all WASM extensions against current WIT
run: ./scripts/build-wasm-extensions.sh
- name: Instantiation test (host linker compatibility)
@@ -172,6 +218,8 @@ jobs:
bench-compile:
name: Benchmark Compilation
needs: changes
if: needs.changes.outputs.has_code == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -224,9 +272,15 @@ jobs:
name: Run Tests
runs-on: ubuntu-latest
if: always()
needs: [tests, heavy-integration-tests, telegram-tests, wasm-wit-compat, docker-build, windows-build, version-check, bench-compile]
needs: [changes, matrix-config, tests, heavy-integration-tests, telegram-tests, wasm-wit-compat, docker-build, windows-build, version-check, bench-compile]
steps:
- run: |
# If no code changes on a PR, expensive jobs were correctly skipped
if [[ "${{ needs.changes.outputs.has_code }}" == "false" ]]; then
echo "No code changes — all tests skipped correctly"
exit 0
fi
# Unit tests must always pass
if [[ "${{ needs.tests.result }}" != "success" ]]; then
echo "Unit tests failed"

View File

@@ -39,6 +39,7 @@ I18n.register('ko', {
'tab.chat': '채팅',
'tab.memory': '기억',
'tab.jobs': '작업',
'tab.projects': '프로젝트',
'tab.missions': '미션',
'tab.routines': '루틴',
'tab.settings': '설정',

View File

@@ -39,6 +39,7 @@ I18n.register('zh-CN', {
'tab.chat': '聊天',
'tab.memory': '记忆',
'tab.jobs': '任务',
'tab.projects': '项目',
'tab.missions': '使命',
'tab.routines': '定时任务',
'tab.settings': '设置',

View File

@@ -194,21 +194,20 @@ impl LiveTestHarness {
let needle_lc = needle.to_ascii_lowercase();
for event in self.rig.captured_status_events() {
match event {
StatusUpdate::ToolStarted { name, detail, .. }
if name.to_ascii_lowercase().contains(&tool_lc) =>
StatusUpdate::ToolStarted {
name,
detail: Some(d),
..
} if name.to_ascii_lowercase().contains(&tool_lc)
&& d.to_ascii_lowercase().contains(&needle_lc) =>
{
if let Some(d) = detail
&& d.to_ascii_lowercase().contains(&needle_lc)
{
return true;
}
return true;
}
StatusUpdate::ToolResult { name, preview, .. }
if name.to_ascii_lowercase().contains(&tool_lc) =>
if name.to_ascii_lowercase().contains(&tool_lc)
&& preview.to_ascii_lowercase().contains(&needle_lc) =>
{
if preview.to_ascii_lowercase().contains(&needle_lc) {
return true;
}
return true;
}
_ => {}
}