mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
improve(ci): preserve complete main validation during merge bursts (#110298)
* ci: serialize main and bound sticky caches * fix(ci): pin cache warmer to validated SHA * fix(ci): read current sticky retirement manifest
This commit is contained in:
committed by
GitHub
parent
125934addc
commit
8c17d20cd9
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: openclaw-ci-limits
|
||||
description: Manage OpenClaw GitHub Actions and Blacksmith CI capacity, runner-registration budgets, fanout caps, main-push debounce, shard sizing, hosted-runner offload, queue health, and safe ramp-down/ramp-up changes. Use when tuning `.github/workflows/*`, `docs/ci.md`, CI runner labels, matrix `max-parallel`, ClawSweeper/Blacksmith burst protection, CodeQL runner placement, or investigating slow/queued OpenClaw CI.
|
||||
description: Manage OpenClaw GitHub Actions and Blacksmith CI capacity, runner-registration budgets, fanout caps, main-push single-flight, shard sizing, hosted-runner offload, queue health, and safe ramp-down/ramp-up changes. Use when tuning `.github/workflows/*`, `docs/ci.md`, CI runner labels, matrix `max-parallel`, ClawSweeper/Blacksmith burst protection, CodeQL runner placement, or investigating slow/queued OpenClaw CI.
|
||||
---
|
||||
|
||||
# OpenClaw CI Limits
|
||||
@@ -84,12 +84,11 @@ and register more runners before the window resets. Use job duration, retries,
|
||||
and queue turnover to justify any lower estimate. Add non-matrix Blacksmith jobs
|
||||
such as `preflight`, `security-fast`, `build-artifacts`, and platform lanes.
|
||||
|
||||
For repeated pushes, multiply by the number of runs expected to reach
|
||||
Blacksmith admission in the same 5-minute window, including runs canceled after
|
||||
admission. The debounce only suppresses pushes that arrive while
|
||||
`runner-admission` is still sleeping; once Blacksmith jobs register, those
|
||||
registrations are spent even if a later push cancels the run. If timing is
|
||||
uncertain, count every sequential push in the window.
|
||||
For repeated pull-request pushes, multiply by the number of runs expected to
|
||||
reach Blacksmith admission in the same 5-minute window, including runs canceled
|
||||
after admission. Canonical `main` is single-flight: one run completes while
|
||||
GitHub's default single pending slot is replaced by the newest push. Count one
|
||||
active main matrix plus its next pending matrix, not every intermediate merge.
|
||||
|
||||
Reject a change unless the org-level worst case stays below about 60% of the
|
||||
live bucket. With the current 10,000-registration bucket, keep planned
|
||||
@@ -100,10 +99,9 @@ ClawSweeper, ClawHub, Clownfish, OpenClaw RTT, and Clawbench.
|
||||
|
||||
Prefer these in order:
|
||||
|
||||
1. Add or preserve concurrency groups that cancel superseded PR and canonical
|
||||
`main` runs before Blacksmith work starts.
|
||||
2. Keep the `runner-admission` hosted debounce for canonical `main` pushes.
|
||||
Change `OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS` only with evidence.
|
||||
1. Preserve cancel-in-progress for superseded pull-request heads.
|
||||
2. Preserve canonical `main` single-flight without canceling its running
|
||||
integration cycle; GitHub's default pending slot coalesces to the newest tip.
|
||||
3. Move high-frequency, short, non-build jobs to `ubuntu-24.04`.
|
||||
4. Reduce matrix rows by bundling related tests inside one runner job when the
|
||||
combined job stays under timeout and keeps useful failure names.
|
||||
@@ -120,18 +118,17 @@ Do not:
|
||||
- raise all `max-parallel` values at once;
|
||||
- make manual `workflow_dispatch` runs cancel normal push/PR validation;
|
||||
- delete coverage just to reduce runner count;
|
||||
- treat cancelled superseded runs as failures without checking the newest run
|
||||
for the same ref.
|
||||
- treat cancelled superseded pull-request runs as failures without checking the
|
||||
newest run for the same ref.
|
||||
|
||||
## Current OpenClaw Knobs
|
||||
|
||||
These are intentionally guarded by `test/scripts/ci-workflow-guards.test.ts`:
|
||||
|
||||
- `CI` concurrency key version and `cancel-in-progress` for PRs and canonical
|
||||
`main` pushes.
|
||||
- `runner-admission` on `ubuntu-24.04` with
|
||||
`OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS=90`.
|
||||
- `preflight` and `security-fast` needing `runner-admission`.
|
||||
- `CI` concurrency key version, PR cancellation, and non-canceling canonical
|
||||
`main` single-flight with one coalesced pending tip.
|
||||
- `preflight` and hosted `security-fast` start immediately without a debounce
|
||||
or standalone admission job.
|
||||
- CI matrix caps: fast/check lanes at 12, Node test shards at 28, Windows and
|
||||
Android at 2.
|
||||
- Canonical PR Node tests use one precise changed-target job when possible;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -53,22 +53,20 @@ run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.dispatch_id !=
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}
|
||||
# PRs want newest-head feedback. Canonical main instead runs one complete
|
||||
# integration cycle while GitHub's single pending slot coalesces later pushes.
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
# Keep the canonical main queue quiet long enough for a follow-up push to
|
||||
# cancel this run before it registers the Blacksmith matrix.
|
||||
# Preflight: establish routing truth and job matrices once, then let real
|
||||
# work fan out from a single source of truth.
|
||||
preflight:
|
||||
permissions:
|
||||
contents: read
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
env:
|
||||
OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS: "90"
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
@@ -116,10 +114,6 @@ jobs:
|
||||
run_protocol_event_coverage: ${{ steps.manifest.outputs.run_protocol_event_coverage }}
|
||||
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
|
||||
steps:
|
||||
- name: Record debounce epoch
|
||||
if: github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main'
|
||||
run: echo "OPENCLAW_DEBOUNCE_EPOCH=$(date +%s)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate release-gate dispatch
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_gate
|
||||
env:
|
||||
@@ -751,23 +745,6 @@ jobs:
|
||||
if: steps.manifest.outputs.run_protocol_event_coverage == 'true'
|
||||
run: node scripts/check-protocol-event-coverage.mjs
|
||||
|
||||
# Fan-out debounce: every heavy job needs preflight, so holding this job
|
||||
# open until the debounce window elapses lets a superseding main push
|
||||
# cancel the run while only one 4 vCPU runner has been spent. Preflight's
|
||||
# own work usually exceeds the window, making the residual sleep zero.
|
||||
- name: Debounce canonical main fan-out
|
||||
if: github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
elapsed=$(( $(date +%s) - OPENCLAW_DEBOUNCE_EPOCH ))
|
||||
remaining=$(( OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS - elapsed ))
|
||||
if [ "$remaining" -gt 0 ]; then
|
||||
echo "Holding fan-out ${remaining}s for a superseding main push"
|
||||
sleep "$remaining"
|
||||
else
|
||||
echo "Debounce window already elapsed (${elapsed}s)"
|
||||
fi
|
||||
|
||||
# Run dependency-free security checks on a hosted runner in parallel with
|
||||
# scope detection. No downstream job waits for Python/pre-commit setup.
|
||||
security-fast:
|
||||
@@ -1818,8 +1795,8 @@ jobs:
|
||||
timeout-minutes: ${{ matrix.timeout_minutes || 60 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# The canonical main path waits for the admission debounce above, so
|
||||
# widen this large matrix within the current runner-registration budget.
|
||||
# Canonical main admits only one complete run at a time, so widen this
|
||||
# matrix within the current runner-registration budget.
|
||||
max-parallel: 28
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
name: Sticky Disk Cleanup
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
retired_key:
|
||||
description: Exact key listed in .github/retired-sticky-disks.json
|
||||
required: true
|
||||
type: string
|
||||
architecture:
|
||||
description: Blacksmith disk architecture
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- amd64
|
||||
- arm64
|
||||
region:
|
||||
description: Exact Blacksmith disk region listed in the retirement manifest
|
||||
required: true
|
||||
type: string
|
||||
confirm:
|
||||
description: Delete this retired sticky-disk key
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: sticky-disk-cleanup
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
if: github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main' && inputs.confirm
|
||||
runs-on: ${{ inputs.architecture == 'arm64' && 'blacksmith-16vcpu-ubuntu-2404-arm' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout protected manifest
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
# A rerun keeps its original workflow_dispatch SHA. Read the latest
|
||||
# protected manifest so removing an entry revokes deletion authority.
|
||||
ref: refs/heads/main
|
||||
|
||||
- name: Validate exact retired key
|
||||
env:
|
||||
RETIRED_ARCHITECTURE: ${{ inputs.architecture }}
|
||||
RETIRED_KEY: ${{ inputs.retired_key }}
|
||||
RETIRED_REGION: ${{ inputs.region }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'EOF'
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const retiredDisks = JSON.parse(
|
||||
readFileSync(".github/retired-sticky-disks.json", "utf8"),
|
||||
);
|
||||
if (!Array.isArray(retiredDisks)) {
|
||||
throw new Error("retired sticky-disk manifest must be an array");
|
||||
}
|
||||
for (const disk of retiredDisks) {
|
||||
if (
|
||||
typeof disk?.key !== "string" ||
|
||||
disk.key.length === 0 ||
|
||||
disk.key !== disk.key.trim() ||
|
||||
(disk.architecture !== "amd64" && disk.architecture !== "arm64") ||
|
||||
typeof disk.region !== "string" ||
|
||||
disk.region.length === 0 ||
|
||||
disk.region !== disk.region.trim()
|
||||
) {
|
||||
throw new Error(
|
||||
"retired sticky-disk manifest entries require canonical key, architecture, and region",
|
||||
);
|
||||
}
|
||||
}
|
||||
const requestedArchitecture = process.env.RETIRED_ARCHITECTURE;
|
||||
const requestedKey = process.env.RETIRED_KEY;
|
||||
const requestedRegion = process.env.RETIRED_REGION;
|
||||
if (!requestedKey || requestedKey !== requestedKey.trim()) {
|
||||
throw new Error("sticky-disk key must be non-empty and canonical");
|
||||
}
|
||||
if (!requestedRegion || requestedRegion !== requestedRegion.trim()) {
|
||||
throw new Error("sticky-disk region must be non-empty and canonical");
|
||||
}
|
||||
const runnerArchitecture = process.env.BLACKSMITH_ENV?.includes("arm")
|
||||
? "arm64"
|
||||
: "amd64";
|
||||
if (requestedArchitecture !== runnerArchitecture) {
|
||||
throw new Error(
|
||||
`sticky-disk architecture ${requestedArchitecture} does not match runner ${runnerArchitecture}`,
|
||||
);
|
||||
}
|
||||
if (requestedRegion !== process.env.BLACKSMITH_REGION) {
|
||||
throw new Error(
|
||||
`sticky-disk region ${requestedRegion} does not match runner ${process.env.BLACKSMITH_REGION}`,
|
||||
);
|
||||
}
|
||||
const allowlisted = retiredDisks.some(
|
||||
(disk) =>
|
||||
disk?.key === requestedKey &&
|
||||
disk?.architecture === requestedArchitecture &&
|
||||
disk?.region === requestedRegion,
|
||||
);
|
||||
if (!allowlisted) {
|
||||
throw new Error(
|
||||
`sticky-disk identity is not allowlisted for retirement: ${requestedKey} (${requestedArchitecture}, ${requestedRegion})`,
|
||||
);
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Delete retired sticky disk
|
||||
uses: useblacksmith/stickydisk-delete@3bd8d43f9da764c6b80c2cd6db129bdb568c79b6 # v1
|
||||
with:
|
||||
delete-docker-cache: "false"
|
||||
delete-key: ${{ inputs.retired_key }}
|
||||
@@ -19,16 +19,19 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
warm:
|
||||
# Cancelled main runs count as refresh triggers: under merge traffic every
|
||||
# push cancels the prior run, so success-only gating starves the snapshot
|
||||
# for hours (observed 15:34->18:45 stale window) while this job checks out
|
||||
# main tip anyway. Failed runs still skip so a broken main cannot publish.
|
||||
if: github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_run' || (github.event.workflow_run.event == 'push' && (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'cancelled') && github.event.workflow_run.head_branch == 'main'))
|
||||
# Canonical main CI is single-flight, so only a completed green integration
|
||||
# cycle may refresh the dependency snapshot. Scheduled/dispatch runs remain
|
||||
# the transform and compile cache writers.
|
||||
if: github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_run' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main'))
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
# workflow_run's github.sha follows the current default-branch tip,
|
||||
# which can be newer than the green run that authorized this write.
|
||||
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -47,6 +50,40 @@ jobs:
|
||||
use-actions-cache: "false"
|
||||
vitest-fs-cache: ${{ github.event_name != 'workflow_run' && 'true' || 'false' }}
|
||||
|
||||
- name: Maintain dependency store budget
|
||||
shell: bash
|
||||
env:
|
||||
# The current store is well below this ceiling. Prune only after
|
||||
# accumulated retired package versions exceed 8 GiB.
|
||||
OPENCLAW_PNPM_STORE_MAX_KIB: "8388608"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
store_dir="${PNPM_CONFIG_STORE_DIR:?}"
|
||||
before_kib="$(du -sk "$store_dir" | cut -f1)"
|
||||
after_kib="$before_kib"
|
||||
pruned=false
|
||||
|
||||
if [ "$before_kib" -gt "$OPENCLAW_PNPM_STORE_MAX_KIB" ]; then
|
||||
echo "pnpm store is ${before_kib} KiB; pruning above ${OPENCLAW_PNPM_STORE_MAX_KIB} KiB ceiling"
|
||||
PNPM_CONFIG_STORE_DIR="$store_dir" pnpm store prune
|
||||
after_kib="$(du -sk "$store_dir" | cut -f1)"
|
||||
pruned=true
|
||||
else
|
||||
echo "pnpm store is ${before_kib} KiB; below ${OPENCLAW_PNPM_STORE_MAX_KIB} KiB ceiling"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "### Dependency store maintenance"
|
||||
echo
|
||||
echo "- Before: ${before_kib} KiB"
|
||||
echo "- After: ${after_kib} KiB"
|
||||
echo "- Pruned: ${pruned}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "$after_kib" -gt "$OPENCLAW_PNPM_STORE_MAX_KIB" ]; then
|
||||
echo "::warning::pnpm store remains above its 8 GiB maintenance ceiling after prune"
|
||||
fi
|
||||
|
||||
- name: Select broad cache seed
|
||||
if: github.event_name != 'workflow_run'
|
||||
shell: bash
|
||||
|
||||
+17
-14
@@ -10,15 +10,16 @@ read_when:
|
||||
|
||||
OpenClaw CI runs on pushes to `main` (Markdown and `docs/**` paths are ignored
|
||||
at the trigger), on every non-draft pull request, and on manual dispatch.
|
||||
On canonical `main` pushes the `preflight` job holds fan-out until a
|
||||
90-second debounce window has elapsed; the `CI` concurrency group cancels that
|
||||
waiting run when a newer commit lands, so sequential merges do not each
|
||||
register a full Blacksmith matrix. Pull requests and manual dispatches skip
|
||||
the wait. `preflight` classifies the diff and turns expensive lanes off when
|
||||
only unrelated areas changed. Manual `workflow_dispatch` runs intentionally
|
||||
bypass smart scoping and fan out the full graph for release candidates and
|
||||
broad validation. Android lanes stay opt-in through `include_android` (or the
|
||||
`release_gate` input). Release-only plugin coverage lives in the separate
|
||||
Canonical `main` pushes are single-flight: the `CI` concurrency group lets one
|
||||
complete integration cycle run while GitHub keeps only the newest pending push.
|
||||
New merges replace that pending run instead of canceling work that already
|
||||
registered a Blacksmith matrix. Pull requests still cancel superseded heads,
|
||||
and manual dispatches use isolated groups. `preflight` classifies the diff and
|
||||
turns expensive lanes off when only unrelated areas changed. Manual
|
||||
`workflow_dispatch` runs intentionally bypass smart scoping and fan out the
|
||||
full graph for release candidates and broad validation. Android lanes stay
|
||||
opt-in through `include_android` (or the `release_gate` input). Release-only
|
||||
plugin coverage lives in the separate
|
||||
[`Plugin Prerelease`](#plugin-prerelease) workflow and only runs from
|
||||
[`Full Release Validation`](#full-release-validation) or an explicit manual
|
||||
dispatch.
|
||||
@@ -56,7 +57,7 @@ Standalone Periphery workflows enforce zero dead-code findings for the iOS and m
|
||||
|
||||
## Fail-fast order
|
||||
|
||||
1. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs. On canonical `main` pushes its final step holds fan-out until a 90-second debounce window has elapsed, so a superseding push cancels the run while only one runner has been spent.
|
||||
1. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs. Canonical `main` starts immediately, but its concurrency group admits only one complete run and coalesces later pushes into one newest pending run.
|
||||
2. `security-fast`, `check-*`, `check-additional-*`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs.
|
||||
3. `build-artifacts` and the advisory `control-ui-i18n` check overlap with the fast Linux lanes. Source PRs exclude generated locale snapshots; the standalone refresh workflow repairs and auto-merges an isolated generated PR in the background. Canonical `release/YYYY.M.PATCH` branches may include release-prep locale repairs with the other generated release output.
|
||||
4. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-*`, `checks-windows`, `macos-node`, `macos-swift`, `ios-build`, and `android`.
|
||||
@@ -69,7 +70,9 @@ replace the separate strict, App-owned test-merge check against current `main`.
|
||||
A later pending or failed rerun does not erase an earlier successful result for
|
||||
that unchanged head during the freshness window.
|
||||
|
||||
GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs. The plugin-list startup-memory guard keeps a 350 MiB ceiling on self-hosted Blacksmith Linux and allows 425 MiB on GitHub-hosted Linux, whose RSS baseline is higher for the same built CLI.
|
||||
The default-branch ruleset requires the GitHub Actions-owned `openclaw/ci-gate` check without a repository-role bypass. The separate strict App-owned test-merge check still binds the head to current `main`. A maintainer therefore cannot manually merge a head whose selected CI lanes failed, as happened before this rule was enforced.
|
||||
|
||||
GitHub may mark superseded pull-request jobs as `cancelled` when a newer head lands. Treat that as CI noise unless the newest run for the same PR is also failing. Canonical `main` runs are not canceled after admission; when merge traffic arrives, GitHub replaces only the older pending run with the newest tip. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs. The plugin-list startup-memory guard keeps a 350 MiB ceiling on self-hosted Blacksmith Linux and allows 425 MiB on GitHub-hosted Linux, whose RSS baseline is higher for the same built CLI.
|
||||
|
||||
Use `pnpm ci:timings`, `pnpm ci:timings:recent`, or `node scripts/ci-run-timings.mjs <run-id>` to summarize wall time, queue time, slowest jobs, failures, and the `pnpm-store-warmup` fanout barrier from GitHub Actions. The in-workflow `ci-timings-summary` job exists in `ci.yml` but is currently disabled (`if: false`); run the timing helper locally instead. For build timing, check the `build-artifacts` job's `Build dist` step: `pnpm build:ci-artifacts` prints `[build-all] phase timings:` and includes `ui:build`; the job also uploads the `startup-memory` artifact.
|
||||
|
||||
@@ -109,11 +112,11 @@ The slowest Node test families are split or balanced so each job stays small wit
|
||||
- Auto-reply runs as balanced workers, with the reply subtree split into agent-runner, commands, dispatch, session, and state-routing shards.
|
||||
- Agentic gateway/server (control-plane) configs split across chat, auth, model, HTTP/plugin, runtime, and startup lanes instead of waiting on built artifacts.
|
||||
- Normal CI packs only isolated infra include-pattern shards into deterministic bundles of at most 64 test files, reducing the Node matrix without merging non-isolated command/cron, stateful agents-core, or gateway/server suites. Heavy fixed suites stay on 8 vCPU while the bundled and lower-weight lanes use 4 vCPU.
|
||||
- Pull requests on the canonical repository reuse the changed-test resolver against the synthetic merged-tree diff. Precise changes run one targeted Node job; each selected test file gets its own process so stateful suite isolation remains intact. The planner combines sibling tests with import-graph dependents and falls back to the existing 14-job compact full-suite plan for workspace package, package/lockfile, shared harness, split-config, renamed, or deleted changes, public extension-contract changes, tests with special shard setup, partially resolved or empty targets, oversized path or target plans, and planner errors. Targeted plans always retain the full built-artifact boundary gate because its repository scanners cannot be derived from imports. `main` pushes run the same full compact suite (never diff-targeted plans, because canceled superseded `main` runs make a single-push diff insufficient as integration proof); manual dispatches and release gates retain the full named per-shard matrix.
|
||||
- Pull requests on the canonical repository reuse the changed-test resolver against the synthetic merged-tree diff. Precise changes run one targeted Node job; each selected test file gets its own process so stateful suite isolation remains intact. The planner combines sibling tests with import-graph dependents and falls back to the existing 14-job compact full-suite plan for workspace package, package/lockfile, shared harness, split-config, renamed, or deleted changes, public extension-contract changes, tests with special shard setup, partially resolved or empty targets, oversized path or target plans, and planner errors. Targeted plans always retain the full built-artifact boundary gate because its repository scanners cannot be derived from imports. `main` pushes run the same full compact suite: pending intermediate push events can be coalesced, so the newest surviving run must validate the complete integration tree rather than only its final single-push diff. Manual dispatches and release gates retain the full named per-shard matrix.
|
||||
- The full Node matrix admits the consistently slow serial tooling, auto-reply command shards, and broad core-fast cache writer first. This keeps the 28-job cap while preventing critical-path work and the next run's transform seed from slipping into a later wave.
|
||||
- Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard.
|
||||
- Linux Node shard jobs persist Vitest's experimental filesystem module cache. Trusted Blacksmith jobs clone one protected disk per platform and Node line; pull requests write only to their private clone and discard it, so PR traffic cannot allocate backing disks or publish feature-branch transforms. GitHub-hosted and fork jobs use an `actions/cache` fallback with coarse PR-scoped restore prefixes. The planner marks the broad `core-unit-fast` graph as the single writer without coupling cache ownership to matrix order. Concurrent Vitest workers retain separate live directories. A transform-input fingerprint clears incompatible lockfile, package, tsconfig, and Vitest-config generations inside the stable disk. Only a protected writer scans and prunes the cache to 75% after it exceeds 2 GiB. A non-cancelling daily or default-branch repository-dispatch warmer refreshes the protected seed; GitHub's normal cache eviction expires fallback PR archives.
|
||||
- Trusted Linux Node jobs also bind the pnpm store and `node_modules` from one protected dependency disk per supported Node line. Package manifests, install settings, runner platform, and the exact Node patch stay out of the disk key; an exact runtime and install-input fingerprint decides whether a job reuses the tree or reinstalls and refreshes the same disk. Manifests are canonicalized before hashing. The audited direct root hooks retain only pnpm's install lifecycle scripts, so formatting and ordinary test/build script edits keep the warm dependency tree; unaudited lifecycle-hook drift fails closed until its source inputs join the fingerprint contract. Dependency, package-manager, hook-source, and lockfile changes always invalidate the snapshot. A pull request whose read-only snapshot has a different fingerprint detaches the workspace bind and installs into runner-local storage, avoiding slow writes to a clone it cannot publish. Sticky cold installs disable pnpm's inner fetch retries and make up to three bounded full-install attempts from the progressively warmed store; a timeout remains a failure. After an exact restore or frozen-lockfile install, setup disables pnpm's redundant pre-run dependency check: the repository intentionally prunes plugin-local `node_modules`, which pnpm otherwise treats as stale and repairs through unsafe concurrent implicit installs during shard fanout. The non-cancelled cache warmer is the only writer: successful `main` CI completion coalesces a dependency refresh, while the daily run remains a deadline fallback. Required CI jobs and pull requests get disposable clones, so dependency changes do not create new disks, competing snapshots, or a cache lock that can cancel builds.
|
||||
- Trusted Linux Node jobs also bind the pnpm store and `node_modules` from one protected dependency disk per supported Node line. Package manifests, install settings, runner platform, and the exact Node patch stay out of the disk key; an exact runtime and install-input fingerprint decides whether a job reuses the tree or reinstalls and refreshes the same disk. Manifests are canonicalized before hashing. The audited direct root hooks retain only pnpm's install lifecycle scripts, so formatting and ordinary test/build script edits keep the warm dependency tree; unaudited lifecycle-hook drift fails closed until its source inputs join the fingerprint contract. Dependency, package-manager, hook-source, and lockfile changes always invalidate the snapshot. A pull request whose read-only snapshot has a different fingerprint detaches the workspace bind and installs into runner-local storage, avoiding slow writes to a clone it cannot publish. Sticky cold installs disable pnpm's inner fetch retries and make up to three bounded full-install attempts from the progressively warmed store; a timeout remains a failure. After an exact restore or frozen-lockfile install, setup disables pnpm's redundant pre-run dependency check: the repository intentionally prunes plugin-local `node_modules`, which pnpm otherwise treats as stale and repairs through unsafe concurrent implicit installs during shard fanout. The non-cancelled cache warmer is the only writer: successful `main` CI completion coalesces a dependency refresh, while the daily run remains a deadline fallback. That writer measures the store on every refresh and runs `pnpm store prune` only after retired package versions push it above 8 GiB. Required CI jobs and pull requests get disposable clones, so dependency changes do not create new disks, competing snapshots, or a cache lock that can cancel builds.
|
||||
- Node shard and build-artifact jobs also restore Node's portable on-disk compile cache. Independent `test` and `build` namespaces prevent their writers from replacing each other's snapshots: the scheduled test warmer owns the protected test seed, while `build-artifacts` publishes the protected build seed only from trusted `main` pushes. PR jobs read protected snapshots without publishing feature-branch bytecode; fallback archives remain PR-scoped. This reuses V8 bytecode for Node-loaded orchestration, build tooling, and external dependencies across different checkout paths, including when only part of the source graph changes. Vitest child processes disable an inherited compile cache because coverage can be enabled inside dynamic configs and V8 coverage can lose source-position precision when scripts are deserialized from bytecode.
|
||||
- The build-artifact job also persists content-fingerprinted `build-all` step outputs. CI's self-built plugin SDK declarations hash the complete repository-owned TypeScript/JSON source graph, exclude installed and generated directories, and restore both flat declarations and package bridges after `tsdown` clears `dist`. Documentation, workflow, plugin, and other changes outside that graph can reuse the declaration snapshot; source changes rebuild it before the export gate runs.
|
||||
- Full declaration builds split `tsdown` into AI, workspace-package, and unified groups. Each group caches declarations only, then still rebuilds runtime JavaScript before restoring those declarations. Core or plugin changes therefore invalidate only the large unified graph, while workspace-package changes conservatively invalidate every dependent declaration group. Public full builds generally use an immutable Actions cache; coarse restore keys seed partial changes, per-group content fingerprints reject stale data, and GitHub's cache quota evicts old generations. The weekly Node 22 lane instead publishes a 14-day artifact after successful `main` runs and restores only artifacts whose immutable producer identity resolves to that workflow on `main`, avoiding quota churn without allowing PR code to write a shared cache. Private-QA declarations are never persisted in Actions caches because cache namespaces are not confidentiality boundaries.
|
||||
@@ -128,7 +131,7 @@ job budget.
|
||||
|
||||
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest` and then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles the flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push. Each current Gradle task has one protected sticky disk; PR jobs use disposable clones, while protected runs refresh content-addressed Gradle entries in place.
|
||||
|
||||
Blacksmith sticky-disk keys are deliberately bounded by supported runtime or task dimensions, never PR number, commit, run, branch, or dependency hash. After a key-version migration, maintainers remove obsolete entries in Blacksmith's Sticky Disks dashboard; GitHub Actions fallback archives use GitHub's normal cache eviction.
|
||||
Blacksmith sticky-disk keys are deliberately bounded by supported runtime or task dimensions, never PR number, commit, run, branch, or dependency hash. After a key-version migration, add only the exact obsolete key, architecture, and region identities to `.github/retired-sticky-disks.json`, dispatch `Sticky Disk Cleanup` from `main` with the same dimensions and confirmation, verify deletion, then remove those entries. The workflow routes ARM identities to an ARM runner, rejects runner-region mismatches, uses Blacksmith's exact-key deletion action, and never deletes Docker builder caches or wildcard prefixes. GitHub Actions fallback archives use GitHub's normal cache eviction.
|
||||
|
||||
The `check-dependencies` shard runs production Knip dependency, unused-file, and unused-export checks. The unused-file guard fails when a PR adds a new unreviewed unused file or leaves a stale allowlist entry, while preserving intentional dynamic plugin, generated, build, live-test, and package bridge surfaces that Knip cannot resolve statically. The unused-export guard excludes test-support files and fails on every unused production export; intentional dynamic consumers must be modeled in `config/knip.config.ts`. Historical targets run the export guard when they provide it and retain their older dead-code fallback otherwise.
|
||||
|
||||
|
||||
@@ -1844,29 +1844,23 @@ describe("ci workflow guards", () => {
|
||||
expect(runStep.run).toContain(":benchmark:assembleDebug");
|
||||
});
|
||||
|
||||
it("debounces canonical main fan-out inside preflight", () => {
|
||||
it("runs canonical main CI single-flight while coalescing the pending tip", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const source = readFileSync(".github/workflows/ci.yml", "utf8");
|
||||
|
||||
// The debounce lives at the tail of preflight: heavy jobs all need
|
||||
// preflight, so a superseding main push can cancel the run before fan-out
|
||||
// while only one runner has been spent. No standalone admission job may
|
||||
// reappear on the critical path.
|
||||
// GitHub concurrency keeps one running and one pending run by default.
|
||||
// Replacing only the pending run preserves a complete integration cycle
|
||||
// while coalescing merge bursts to the newest main tip.
|
||||
expect(workflow.concurrency["cancel-in-progress"]).toBe(
|
||||
"${{ github.event_name == 'pull_request' }}",
|
||||
);
|
||||
expect(workflow.jobs["runner-admission"]).toBeUndefined();
|
||||
const preflight = workflow.jobs.preflight;
|
||||
expect(preflight.needs).toBeUndefined();
|
||||
expect(preflight.env.OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS).toBe("90");
|
||||
expect(preflight.env?.OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS).toBeUndefined();
|
||||
const steps = preflight.steps as Array<{ if?: string; name?: string; run?: string }>;
|
||||
expect(steps[0]?.name).toBe("Record debounce epoch");
|
||||
expect(steps[0]?.if).toContain("github.ref == 'refs/heads/main'");
|
||||
const gate = steps.at(-1);
|
||||
expect(gate?.name).toBe("Debounce canonical main fan-out");
|
||||
expect(gate?.if).toContain("github.ref == 'refs/heads/main'");
|
||||
expect(gate?.run).toContain('sleep "$remaining"');
|
||||
expect(steps.some((step) => step.name === "Record debounce epoch")).toBe(false);
|
||||
expect(steps.some((step) => step.name === "Debounce canonical main fan-out")).toBe(false);
|
||||
expect(workflow.jobs["security-fast"].needs).toBeUndefined();
|
||||
expect(source).toContain(
|
||||
"cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps CodeQL critical quality scans off Blacksmith registrations", () => {
|
||||
@@ -2490,18 +2484,29 @@ describe("ci workflow guards", () => {
|
||||
const warmerSetup = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Setup Node environment",
|
||||
);
|
||||
const checkoutStep = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Checkout",
|
||||
);
|
||||
const seedStep = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Select broad cache seed",
|
||||
);
|
||||
const warmStep = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Warm transform and compile caches",
|
||||
);
|
||||
const maintainStoreStep = warmer.jobs.warm.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Maintain dependency store budget",
|
||||
);
|
||||
|
||||
expect(warmer.concurrency["cancel-in-progress"]).toBe(false);
|
||||
expect(warmer.concurrency.group).toBe("vitest-cache-warm");
|
||||
expect(warmer.on.workflow_dispatch).toBeUndefined();
|
||||
expect(warmer.on.repository_dispatch.types).toEqual(["vitest-cache-warm"]);
|
||||
expect(warmer.jobs.warm.if).toContain("github.repository == 'openclaw/openclaw'");
|
||||
expect(warmer.jobs.warm.if).toContain("github.event.workflow_run.conclusion == 'success'");
|
||||
expect(warmer.jobs.warm.if).not.toContain("cancelled");
|
||||
expect(checkoutStep.with.ref).toBe(
|
||||
"${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}",
|
||||
);
|
||||
expect(warmerSource).toContain('cron: "17 8 * * *"');
|
||||
expect(warmerSource).toContain('candidate.shardName.startsWith("core-unit-fast")');
|
||||
expect(warmerSetup.with).toMatchObject({
|
||||
@@ -2520,6 +2525,34 @@ describe("ci workflow guards", () => {
|
||||
);
|
||||
expect(seedStep.if).toBe("github.event_name != 'workflow_run'");
|
||||
expect(warmStep.if).toBe("github.event_name != 'workflow_run'");
|
||||
expect(warmer.jobs.warm.steps.indexOf(warmerSetup)).toBeLessThan(
|
||||
warmer.jobs.warm.steps.indexOf(maintainStoreStep),
|
||||
);
|
||||
expect(maintainStoreStep.env.OPENCLAW_PNPM_STORE_MAX_KIB).toBe("8388608");
|
||||
expect(maintainStoreStep.run).toContain('store_dir="${PNPM_CONFIG_STORE_DIR:?}"');
|
||||
expect(maintainStoreStep.run).toContain('PNPM_CONFIG_STORE_DIR="$store_dir" pnpm store prune');
|
||||
expect(maintainStoreStep.run).toContain('>> "$GITHUB_STEP_SUMMARY"');
|
||||
|
||||
const maintenanceRoot = mkdtempSync(path.join(tmpdir(), "openclaw-pnpm-maintenance-"));
|
||||
try {
|
||||
const storeDir = path.join(maintenanceRoot, "store");
|
||||
const summaryPath = path.join(maintenanceRoot, "summary.md");
|
||||
mkdirSync(storeDir);
|
||||
const result = spawnSync("bash", ["-c", maintainStoreStep.run], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
GITHUB_STEP_SUMMARY: summaryPath,
|
||||
OPENCLAW_PNPM_STORE_MAX_KIB: "-1",
|
||||
PNPM_CONFIG_STORE_DIR: storeDir,
|
||||
},
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout).toContain("pruning above -1 KiB ceiling");
|
||||
expect(readFileSync(summaryPath, "utf8")).toContain("- Pruned: true");
|
||||
} finally {
|
||||
rmSync(maintenanceRoot, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses bundled Node shards and telemetry-backed runner sizes", () => {
|
||||
@@ -2695,6 +2728,131 @@ describe("ci workflow guards", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("deletes only exact allowlisted retired sticky disks from protected main", () => {
|
||||
const cleanupSource = readFileSync(".github/workflows/sticky-disk-cleanup.yml", "utf8");
|
||||
const cleanup = parse(cleanupSource);
|
||||
const job = cleanup.jobs.delete;
|
||||
const checkoutStep = job.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Checkout protected manifest",
|
||||
);
|
||||
const validateStep = job.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Validate exact retired key",
|
||||
);
|
||||
const deleteStep = job.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Delete retired sticky disk",
|
||||
);
|
||||
const retiredDisks = JSON.parse(
|
||||
readFileSync(".github/retired-sticky-disks.json", "utf8"),
|
||||
) as Array<{ architecture?: unknown; key?: unknown; region?: unknown }>;
|
||||
|
||||
expect(Array.isArray(retiredDisks)).toBe(true);
|
||||
expect(
|
||||
retiredDisks.every(
|
||||
(disk) =>
|
||||
typeof disk.key === "string" &&
|
||||
disk.key.length > 0 &&
|
||||
disk.key === disk.key.trim() &&
|
||||
(disk.architecture === "amd64" || disk.architecture === "arm64") &&
|
||||
typeof disk.region === "string" &&
|
||||
disk.region.length > 0 &&
|
||||
disk.region === disk.region.trim(),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
new Set(retiredDisks.map((disk) => `${disk.key}:${disk.architecture}:${disk.region}`)).size,
|
||||
).toBe(retiredDisks.length);
|
||||
expect(cleanup.on).toHaveProperty("workflow_dispatch");
|
||||
expect(cleanup.permissions).toEqual({ contents: "read" });
|
||||
expect(cleanup.concurrency).toEqual({
|
||||
group: "sticky-disk-cleanup",
|
||||
"cancel-in-progress": false,
|
||||
});
|
||||
expect(job.if).toContain("github.ref == 'refs/heads/main'");
|
||||
expect(job.if).toContain("inputs.confirm");
|
||||
expect(checkoutStep.with.ref).toBe("refs/heads/main");
|
||||
expect(job["runs-on"]).toContain("inputs.architecture == 'arm64'");
|
||||
expect(validateStep.env.RETIRED_ARCHITECTURE).toBe("${{ inputs.architecture }}");
|
||||
expect(validateStep.env.RETIRED_KEY).toBe("${{ inputs.retired_key }}");
|
||||
expect(validateStep.env.RETIRED_REGION).toBe("${{ inputs.region }}");
|
||||
expect(validateStep.run).toContain('process.env.BLACKSMITH_ENV?.includes("arm")');
|
||||
expect(validateStep.run).toContain("requestedRegion !== process.env.BLACKSMITH_REGION");
|
||||
expect(validateStep.run).toContain("requestedKey !== requestedKey.trim()");
|
||||
expect(validateStep.run).toContain("disk?.key === requestedKey");
|
||||
const rejectedKey = spawnSync("bash", ["-c", validateStep.run], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
BLACKSMITH_ENV: "production-amd64",
|
||||
BLACKSMITH_REGION: "us-test-1",
|
||||
RETIRED_ARCHITECTURE: "amd64",
|
||||
RETIRED_KEY: "openclaw/openclaw-not-retired",
|
||||
RETIRED_REGION: "us-test-1",
|
||||
},
|
||||
});
|
||||
expect(rejectedKey.status).not.toBe(0);
|
||||
expect(rejectedKey.stderr).toContain("identity is not allowlisted for retirement");
|
||||
const paddedKey = spawnSync("bash", ["-c", validateStep.run], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
BLACKSMITH_ENV: "production-amd64",
|
||||
BLACKSMITH_REGION: "us-test-1",
|
||||
RETIRED_ARCHITECTURE: "amd64",
|
||||
RETIRED_KEY: " openclaw/openclaw-active-key ",
|
||||
RETIRED_REGION: "us-test-1",
|
||||
},
|
||||
});
|
||||
expect(paddedKey.status).not.toBe(0);
|
||||
expect(paddedKey.stderr).toContain("key must be non-empty and canonical");
|
||||
expect(deleteStep).toMatchObject({
|
||||
uses: "useblacksmith/stickydisk-delete@3bd8d43f9da764c6b80c2cd6db129bdb568c79b6",
|
||||
with: {
|
||||
"delete-docker-cache": "false",
|
||||
"delete-key": "${{ inputs.retired_key }}",
|
||||
},
|
||||
});
|
||||
|
||||
// A retired-key entry must never match any disk family still mounted by
|
||||
// the repository. Expressions stand for one non-empty resolved segment.
|
||||
const workflowFiles = readdirSync(".github/workflows")
|
||||
.filter((name) => name.endsWith(".yml"))
|
||||
.map((name) => `.github/workflows/${name}`);
|
||||
const actionFiles = readdirSync(".github/actions").map(
|
||||
(name) => `.github/actions/${name}/action.yml`,
|
||||
);
|
||||
const activeKeyPatterns: RegExp[] = [];
|
||||
for (const file of [...workflowFiles, ...actionFiles]) {
|
||||
if (!existsSync(file)) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parse(readFileSync(file, "utf8"));
|
||||
const jobs = parsed?.jobs ? Object.values(parsed.jobs) : [];
|
||||
const stepLists = [
|
||||
...jobs.map((candidate) => (candidate as { steps?: WorkflowStep[] }).steps ?? []),
|
||||
(parsed?.runs?.steps ?? []) as WorkflowStep[],
|
||||
];
|
||||
for (const step of stepLists.flat()) {
|
||||
if (typeof step?.uses !== "string" || !step.uses.startsWith("useblacksmith/stickydisk@")) {
|
||||
continue;
|
||||
}
|
||||
const key = step.with?.key;
|
||||
if (typeof key !== "string") {
|
||||
continue;
|
||||
}
|
||||
const escapedParts = key
|
||||
.split(/\$\{\{[^}]+\}\}/u)
|
||||
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"));
|
||||
activeKeyPatterns.push(new RegExp(`^${escapedParts.join(".+")}$`, "u"));
|
||||
}
|
||||
}
|
||||
for (const retiredDisk of retiredDisks) {
|
||||
expect(
|
||||
activeKeyPatterns.some((pattern) => pattern.test(retiredDisk.key as string)),
|
||||
`${retiredDisk.key} is still an active sticky-disk key`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("runs the session accessor ratchet as a visible additional check", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const additionalJob = workflow.jobs["check-additional-shard"];
|
||||
|
||||
Reference in New Issue
Block a user