Merge pull request #2850 from zeroclaw-labs/pr/ci-guardrails-20260305

ci: add PR binary-size regression and release size parity
This commit is contained in:
Argenis 2026-03-05 09:49:01 -05:00 committed by GitHub
commit 2bdc17e5af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 282 additions and 4 deletions

View File

@ -57,6 +57,9 @@ jobs:
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- name: Capture lint job start timestamp
shell: bash
run: echo "CI_JOB_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV"
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
@ -75,7 +78,8 @@ jobs:
- name: Ensure cargo component
shell: bash
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
- id: rust-cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-check
cache-bin: false
@ -85,6 +89,19 @@ jobs:
env:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
run: ./scripts/ci/rust_strict_delta_gate.sh
- name: Publish lint telemetry
if: always()
shell: bash
run: |
set -euo pipefail
now="$(date +%s)"
start="${CI_JOB_STARTED_AT:-$now}"
elapsed="$((now - start))"
{
echo "### CI Telemetry: lint"
echo "- rust-cache hit: \`${{ steps.rust-cache.outputs.cache-hit || 'unknown' }}\`"
echo "- Duration (s): \`${elapsed}\`"
} >> "$GITHUB_STEP_SUMMARY"
workspace-check:
name: Workspace Check
@ -143,6 +160,9 @@ jobs:
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- name: Capture test job start timestamp
shell: bash
run: echo "CI_JOB_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV"
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
@ -158,7 +178,8 @@ jobs:
- name: Ensure cargo component
shell: bash
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
- id: rust-cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-check
cache-bin: false
@ -210,10 +231,15 @@ jobs:
if [ -f artifacts/flake-probe.json ]; then
status=$(python3 -c "import json; print(json.load(open('artifacts/flake-probe.json'))['status'])")
flake=$(python3 -c "import json; print(json.load(open('artifacts/flake-probe.json'))['flake_suspected'])")
now="$(date +%s)"
start="${CI_JOB_STARTED_AT:-$now}"
elapsed="$((now - start))"
{
echo "### Test Flake Probe"
echo "- Status: \`${status}\`"
echo "- Flake suspected: \`${flake}\`"
echo "- rust-cache hit: \`${{ steps.rust-cache.outputs.cache-hit || 'unknown' }}\`"
echo "- Duration (s): \`${elapsed}\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload flake probe artifact
@ -263,6 +289,9 @@ jobs:
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target
steps:
- name: Capture build job start timestamp
shell: bash
run: echo "CI_JOB_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV"
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
@ -278,7 +307,8 @@ jobs:
- name: Ensure cargo component
shell: bash
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
- id: rust-cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-build
cache-targets: true
@ -294,6 +324,83 @@ jobs:
BINARY_SIZE_ADVISORY_MB: 20
BINARY_SIZE_TARGET_MB: 5
run: bash scripts/ci/check_binary_size.sh target/release-fast/zeroclaw
- name: Publish build telemetry
if: always()
shell: bash
run: |
set -euo pipefail
now="$(date +%s)"
start="${CI_JOB_STARTED_AT:-$now}"
elapsed="$((now - start))"
{
echo "### CI Telemetry: build"
echo "- rust-cache hit: \`${{ steps.rust-cache.outputs.cache-hit || 'unknown' }}\`"
echo "- Duration (s): \`${elapsed}\`"
} >> "$GITHUB_STEP_SUMMARY"
binary-size-regression:
name: Binary Size Regression (PR)
needs: [changes]
if: github.event_name == 'pull_request' && needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 120
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target-head
steps:
- name: Capture binary-size regression job start timestamp
shell: bash
run: echo "CI_JOB_STARTED_AT=$(date +%s)" >> "$GITHUB_ENV"
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- id: rust-cache
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-binary-size-regression
cache-bin: false
- name: Build head binary
shell: bash
run: cargo build --profile release-fast --locked --bin zeroclaw
- name: Compare binary size against base branch
shell: bash
env:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
BINARY_SIZE_REGRESSION_MAX_PERCENT: 10
run: |
set -euo pipefail
bash scripts/ci/check_binary_size_regression.sh \
"$BASE_SHA" \
"$CARGO_TARGET_DIR/release-fast/zeroclaw" \
"${BINARY_SIZE_REGRESSION_MAX_PERCENT}"
- name: Publish binary-size regression telemetry
if: always()
shell: bash
run: |
set -euo pipefail
now="$(date +%s)"
start="${CI_JOB_STARTED_AT:-$now}"
elapsed="$((now - start))"
{
echo "### CI Telemetry: binary-size-regression"
echo "- rust-cache hit: \`${{ steps.rust-cache.outputs.cache-hit || 'unknown' }}\`"
echo "- Duration (s): \`${elapsed}\`"
} >> "$GITHUB_STEP_SUMMARY"
cross-platform-vm:
name: Cross-Platform VM (${{ matrix.name }})
@ -527,7 +634,7 @@ jobs:
ci-required:
name: CI Required Gate
if: always()
needs: [changes, lint, workspace-check, package-check, test, restricted-hermetic, build, cross-platform-vm, linux-distro-container, docker-smoke, docs-only, non-rust, docs-quality, lint-feedback, license-file-owner-guard]
needs: [changes, lint, workspace-check, package-check, test, restricted-hermetic, build, binary-size-regression, cross-platform-vm, linux-distro-container, docker-smoke, docs-only, non-rust, docs-quality, lint-feedback, license-file-owner-guard]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Enforce required status
@ -589,6 +696,7 @@ jobs:
cross_platform_vm_result="${{ needs.cross-platform-vm.result }}"
linux_distro_container_result="${{ needs.linux-distro-container.result }}"
docker_smoke_result="${{ needs.docker-smoke.result }}"
binary_size_regression_result="${{ needs.binary-size-regression.result }}"
echo "lint=${lint_result}"
echo "workspace-check=${workspace_check_result}"
@ -599,6 +707,7 @@ jobs:
echo "cross-platform-vm=${cross_platform_vm_result}"
echo "linux-distro-container=${linux_distro_container_result}"
echo "docker-smoke=${docker_smoke_result}"
echo "binary-size-regression=${binary_size_regression_result}"
echo "docs=${docs_result}"
echo "license_file_owner_guard=${license_owner_result}"
@ -609,6 +718,11 @@ jobs:
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$binary_size_regression_result" != "success" ]; then
echo "Binary size regression guard did not pass for PR."
exit 1
fi
check_docs_quality
echo "All required checks passed."

View File

@ -438,6 +438,45 @@ jobs:
BINARY_SIZE_TARGET_MB: 5
run: bash scripts/ci/check_binary_size.sh "target/${{ matrix.target }}/release-fast/${{ matrix.artifact }}" "${{ matrix.target }}"
- name: Check binary size (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
BINARY_SIZE_HARD_LIMIT_MB: 28
BINARY_SIZE_ADVISORY_MB: 20
BINARY_SIZE_TARGET_MB: 5
run: |
$binaryPath = "target/${{ matrix.target }}/release-fast/${{ matrix.artifact }}"
if (-not (Test-Path $binaryPath)) {
Write-Output "::error::Binary not found at $binaryPath"
exit 1
}
$sizeBytes = (Get-Item $binaryPath).Length
$sizeMB = [math]::Floor($sizeBytes / 1MB)
$hardLimitBytes = [int64]$env:BINARY_SIZE_HARD_LIMIT_MB * 1MB
$advisoryLimitBytes = [int64]$env:BINARY_SIZE_ADVISORY_MB * 1MB
$targetLimitBytes = [int64]$env:BINARY_SIZE_TARGET_MB * 1MB
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### Binary Size: ${{ matrix.target }}"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- Size: ``${sizeMB}MB (${sizeBytes} bytes)``"
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- Limits: hard=``$($env:BINARY_SIZE_HARD_LIMIT_MB)MB`` advisory=``$($env:BINARY_SIZE_ADVISORY_MB)MB`` target=``$($env:BINARY_SIZE_TARGET_MB)MB``"
if ($sizeBytes -gt $hardLimitBytes) {
Write-Output "::error::Binary exceeds $($env:BINARY_SIZE_HARD_LIMIT_MB)MB safeguard (${sizeMB}MB)"
exit 1
}
if ($sizeBytes -gt $advisoryLimitBytes) {
Write-Output "::warning::Binary exceeds $($env:BINARY_SIZE_ADVISORY_MB)MB advisory target (${sizeMB}MB)"
exit 0
}
if ($sizeBytes -gt $targetLimitBytes) {
Write-Output "::warning::Binary exceeds $($env:BINARY_SIZE_TARGET_MB)MB target (${sizeMB}MB)"
exit 0
}
Write-Output "Binary size within target."
- name: Package (Unix)
if: runner.os != 'Windows'
run: |

View File

@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Compare PR binary size against the PR base commit and fail on large regressions.
#
# Usage:
# check_binary_size_regression.sh <base_sha> <head_binary_path> [max_percent_increase]
#
# Behavior:
# - Builds base commit binary with the same release profile (`release-fast`)
# - Emits summary details to GITHUB_STEP_SUMMARY when available
# - Fails only when head binary grows above max_percent_increase
# - Fails open (warning-only) if base build cannot be produced for comparison
set -euo pipefail
BASE_SHA="${1:?Usage: check_binary_size_regression.sh <base_sha> <head_binary_path> [max_percent_increase]}"
HEAD_BIN="${2:?Usage: check_binary_size_regression.sh <base_sha> <head_binary_path> [max_percent_increase]}"
MAX_PERCENT="${3:-10}"
size_bytes() {
local file="$1"
stat -f%z "$file" 2>/dev/null || stat -c%s "$file"
}
if [ ! -f "$HEAD_BIN" ]; then
echo "::error::Head binary not found: ${HEAD_BIN}"
exit 1
fi
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "::warning::Base SHA is not available in this checkout (${BASE_SHA}); skipping binary-size regression gate."
exit 0
fi
HEAD_SIZE="$(size_bytes "$HEAD_BIN")"
tmp_root="${RUNNER_TEMP:-/tmp}"
worktree_dir="$(mktemp -d "${tmp_root%/}/binary-size-base.XXXXXX")"
cleanup() {
git worktree remove --force "$worktree_dir" >/dev/null 2>&1 || true
rm -rf "$worktree_dir" >/dev/null 2>&1 || true
}
trap cleanup EXIT
if ! git worktree add --detach "$worktree_dir" "$BASE_SHA" >/dev/null 2>&1; then
echo "::warning::Failed to create base worktree at ${BASE_SHA}; skipping binary-size regression gate."
exit 0
fi
BASE_TARGET_DIR="${worktree_dir}/target-base"
base_build_status="success"
if ! (
cd "$worktree_dir"
export CARGO_TARGET_DIR="$BASE_TARGET_DIR"
cargo build --profile release-fast --locked --bin zeroclaw
); then
base_build_status="failure"
fi
if [ "$base_build_status" != "success" ]; then
echo "::warning::Base commit build failed at ${BASE_SHA}; skipping binary-size regression gate."
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### Binary Size Regression"
echo "- Base SHA: \`${BASE_SHA}\`"
echo "- Result: skipped (base build failed)"
echo "- Head size bytes: \`${HEAD_SIZE}\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
exit 0
fi
BASE_BIN="${BASE_TARGET_DIR}/release-fast/zeroclaw"
if [ ! -f "$BASE_BIN" ]; then
echo "::warning::Base binary missing (${BASE_BIN}); skipping binary-size regression gate."
exit 0
fi
BASE_SIZE="$(size_bytes "$BASE_BIN")"
DELTA_BYTES="$((HEAD_SIZE - BASE_SIZE))"
DELTA_PERCENT="$(
python3 - "$BASE_SIZE" "$HEAD_SIZE" <<'PY'
import sys
base = int(sys.argv[1])
head = int(sys.argv[2])
if base <= 0:
print("0.00")
else:
pct = ((head - base) / base) * 100.0
print(f"{pct:.2f}")
PY
)"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "### Binary Size Regression"
echo "- Base SHA: \`${BASE_SHA}\`"
echo "- Base size bytes: \`${BASE_SIZE}\`"
echo "- Head size bytes: \`${HEAD_SIZE}\`"
echo "- Delta bytes: \`${DELTA_BYTES}\`"
echo "- Delta percent: \`${DELTA_PERCENT}%\`"
echo "- Max allowed increase: \`${MAX_PERCENT}%\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
if [ "$DELTA_BYTES" -le 0 ]; then
echo "Binary size did not increase vs base (delta=${DELTA_BYTES} bytes)."
exit 0
fi
if ! python3 - "$DELTA_PERCENT" "$MAX_PERCENT" <<'PY'
import sys
delta = float(sys.argv[1])
max_allowed = float(sys.argv[2])
if delta > max_allowed:
sys.exit(1)
sys.exit(0)
PY
then
echo "::error::Binary size regression ${DELTA_PERCENT}% exceeds threshold ${MAX_PERCENT}%."
exit 1
fi
echo "::warning::Binary size increased by ${DELTA_PERCENT}% (within threshold ${MAX_PERCENT}%)."
exit 0