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:
commit
2bdc17e5af
122
.github/workflows/ci-run.yml
vendored
122
.github/workflows/ci-run.yml
vendored
@ -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."
|
||||
|
||||
39
.github/workflows/pub-release.yml
vendored
39
.github/workflows/pub-release.yml
vendored
@ -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: |
|
||||
|
||||
125
scripts/ci/check_binary_size_regression.sh
Executable file
125
scripts/ci/check_binary_size_regression.sh
Executable 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
|
||||
Loading…
Reference in New Issue
Block a user