ci: standardize production pipeline to 8 core workflows

This commit is contained in:
argenis de la rosa 2026-03-03 23:36:59 -05:00
parent b2b93ae861
commit fdabb3c290
34 changed files with 1181 additions and 3823 deletions

View File

@ -1,36 +0,0 @@
# Workflow Directory Layout
GitHub Actions only loads workflow entry files from:
- `.github/workflows/*.yml`
- `.github/workflows/*.yaml`
Subdirectories are not valid locations for workflow entry files.
Repository convention:
1. Keep runnable workflow entry files at `.github/workflows/` root.
2. Keep workflow-only helper scripts under `.github/workflows/scripts/`.
3. Keep cross-tooling/local CI scripts under `scripts/ci/` when they are used outside Actions.
Workflow behavior documentation in this directory:
- `.github/workflows/main-branch-flow.md`
Current workflow helper scripts:
- `.github/workflows/scripts/ci_workflow_owner_approval.js`
- `.github/workflows/scripts/ci_license_file_owner_guard.js`
- `.github/workflows/scripts/lint_feedback.js`
- `.github/workflows/scripts/pr_auto_response_contributor_tier.js`
- `.github/workflows/scripts/pr_auto_response_labeled_routes.js`
- `.github/workflows/scripts/pr_check_status_nudge.js`
- `.github/workflows/scripts/pr_intake_checks.js`
- `.github/workflows/scripts/pr_labeler.js`
- `.github/workflows/scripts/test_benchmarks_pr_comment.js`
Release/CI policy assets introduced for advanced delivery lanes:
- `.github/release/nightly-owner-routing.json`
- `.github/release/canary-policy.json`
- `.github/release/prerelease-stage-gates.json`

View File

@ -0,0 +1,169 @@
name: Auto Main Release Tag
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: auto-main-release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
tag-and-bump:
name: Tag current main + prepare next patch version
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Skip release-prep commits
id: skip
shell: bash
run: |
set -euo pipefail
msg="$(git log -1 --pretty=%B | tr -d '\r')"
if [[ "${msg}" == *"[skip ci]"* && "${msg}" == chore\(release\):\ prepare\ v* ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Enforce release automation actor policy
if: steps.skip.outputs.skip != 'true'
shell: bash
run: |
set -euo pipefail
actor="${GITHUB_ACTOR}"
actor_lc="$(echo "${actor}" | tr '[:upper:]' '[:lower:]')"
allowed_actors_lc="theonlyhennygod,jordanthejet"
if [[ ",${allowed_actors_lc}," != *",${actor_lc},"* ]]; then
echo "::error::Only maintainer actors (${allowed_actors_lc}) can trigger main release tagging. Actor: ${actor}"
exit 1
fi
- name: Resolve current and next version
if: steps.skip.outputs.skip != 'true'
id: version
shell: bash
run: |
set -euo pipefail
current_version="$(awk '
BEGIN { in_pkg=0 }
/^\[package\]/ { in_pkg=1; next }
in_pkg && /^\[/ { in_pkg=0 }
in_pkg && $1 == "version" {
value=$3
gsub(/"/, "", value)
print value
exit
}
' Cargo.toml)"
if [[ -z "${current_version}" ]]; then
echo "::error::Failed to resolve current package version from Cargo.toml"
exit 1
fi
if [[ ! "${current_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Cargo.toml version must be strict semver X.Y.Z (found: ${current_version})"
exit 1
fi
IFS='.' read -r major minor patch <<< "${current_version}"
next_patch="$((patch + 1))"
next_version="${major}.${minor}.${next_patch}"
{
echo "current=${current_version}"
echo "next=${next_version}"
echo "tag=v${current_version}"
} >> "$GITHUB_OUTPUT"
- name: Verify tag does not already exist
id: tag_check
if: steps.skip.outputs.skip != 'true'
shell: bash
run: |
set -euo pipefail
tag="${{ steps.version.outputs.tag }}"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "::warning::Release tag ${tag} already exists on origin; skipping auto-tag/bump for this push."
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Create and push annotated release tag
if: steps.skip.outputs.skip != 'true' && steps.tag_check.outputs.exists != 'true'
shell: bash
run: |
set -euo pipefail
tag="${{ steps.version.outputs.tag }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "${tag}" -m "Release ${tag}"
git push origin "refs/tags/${tag}"
- name: Bump Cargo version for next release
if: steps.skip.outputs.skip != 'true' && steps.tag_check.outputs.exists != 'true'
shell: bash
run: |
set -euo pipefail
next="${{ steps.version.outputs.next }}"
awk -v new_version="${next}" '
BEGIN { in_pkg=0; done=0 }
/^\[package\]/ { in_pkg=1 }
in_pkg && /^\[/ && $0 !~ /^\[package\]/ { in_pkg=0 }
in_pkg && $1 == "version" && done == 0 {
sub(/"[^"]+"/, "\"" new_version "\"")
done=1
}
{ print }
' Cargo.toml > Cargo.toml.tmp
mv Cargo.toml.tmp Cargo.toml
awk -v new_version="${next}" '
BEGIN { in_pkg=0; zc_pkg=0; done=0 }
/^\[\[package\]\]/ { in_pkg=1; zc_pkg=0 }
in_pkg && /^name = "zeroclaw"$/ { zc_pkg=1 }
in_pkg && zc_pkg && /^version = "/ && done == 0 {
sub(/"[^"]+"/, "\"" new_version "\"")
done=1
}
{ print }
' Cargo.lock > Cargo.lock.tmp
mv Cargo.lock.tmp Cargo.lock
- name: Commit and push next-version prep
if: steps.skip.outputs.skip != 'true' && steps.tag_check.outputs.exists != 'true'
shell: bash
run: |
set -euo pipefail
next="${{ steps.version.outputs.next }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
if git diff --cached --quiet; then
echo "No version changes detected; nothing to commit."
exit 0
fi
git commit -m "chore(release): prepare v${next} [skip ci]"
git push origin HEAD:main

View File

@ -1,63 +0,0 @@
name: CI Build (Fast)
# Optional fast release build that runs alongside the normal Build (Smoke) job.
# This workflow is informational and does not gate merges.
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
concurrency:
group: ci-fast-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
changes:
name: Detect Change Scope
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
outputs:
rust_changed: ${{ steps.scope.outputs.rust_changed }}
docs_only: ${{ steps.scope.outputs.docs_only }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Detect docs-only changes
id: scope
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: ./scripts/ci/detect_change_scope.sh
build-fast:
name: Build (Fast)
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 25
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
with:
prefix-key: fast-build
cache-targets: true
- name: Build release binary
run: cargo build --release --locked --verbose

View File

@ -1,329 +0,0 @@
name: CI Canary Gate
on:
workflow_dispatch:
inputs:
mode:
description: "dry-run computes decision only; execute enables canary dispatch"
required: true
default: dry-run
type: choice
options:
- dry-run
- execute
candidate_tag:
description: "Candidate release tag (e.g. v0.1.8-rc.1 or v0.1.8)"
required: false
default: ""
type: string
candidate_sha:
description: "Optional explicit candidate SHA"
required: false
default: ""
type: string
error_rate:
description: "Observed canary error rate (0.0-1.0)"
required: true
default: "0.0"
type: string
crash_rate:
description: "Observed canary crash rate (0.0-1.0)"
required: true
default: "0.0"
type: string
p95_latency_ms:
description: "Observed canary p95 latency in milliseconds"
required: true
default: "0"
type: string
sample_size:
description: "Observed canary sample size"
required: true
default: "0"
type: string
emit_repository_dispatch:
description: "Emit canary decision repository_dispatch event"
required: true
default: false
type: boolean
trigger_rollback_on_abort:
description: "Automatically dispatch CI Rollback Guard when canary decision is abort"
required: true
default: true
type: boolean
rollback_branch:
description: "Rollback integration branch used by CI Rollback Guard dispatch"
required: true
default: dev
type: choice
options:
- dev
- main
rollback_target_ref:
description: "Optional explicit rollback target ref passed to CI Rollback Guard"
required: false
default: ""
type: string
fail_on_violation:
description: "Fail on policy violations"
required: true
default: true
type: boolean
schedule:
- cron: "45 7 * * 1" # Weekly Monday 07:45 UTC
concurrency:
group: canary-gate-${{ github.event.inputs.candidate_tag || github.ref || github.run_id }}
cancel-in-progress: false
permissions:
contents: read
actions: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
canary-plan:
name: Canary Plan
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
outputs:
mode: ${{ steps.inputs.outputs.mode }}
candidate_tag: ${{ steps.inputs.outputs.candidate_tag }}
candidate_sha: ${{ steps.inputs.outputs.candidate_sha }}
trigger_rollback_on_abort: ${{ steps.inputs.outputs.trigger_rollback_on_abort }}
rollback_branch: ${{ steps.inputs.outputs.rollback_branch }}
rollback_target_ref: ${{ steps.inputs.outputs.rollback_target_ref }}
decision: ${{ steps.extract.outputs.decision }}
ready_to_execute: ${{ steps.extract.outputs.ready_to_execute }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Resolve canary inputs
id: inputs
shell: bash
run: |
set -euo pipefail
mode="dry-run"
candidate_tag=""
candidate_sha=""
error_rate="0.0"
crash_rate="0.0"
p95_latency_ms="0"
sample_size="0"
trigger_rollback_on_abort="true"
rollback_branch="dev"
rollback_target_ref=""
fail_on_violation="true"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
mode="${{ github.event.inputs.mode || 'dry-run' }}"
candidate_tag="${{ github.event.inputs.candidate_tag || '' }}"
candidate_sha="${{ github.event.inputs.candidate_sha || '' }}"
error_rate="${{ github.event.inputs.error_rate || '0.0' }}"
crash_rate="${{ github.event.inputs.crash_rate || '0.0' }}"
p95_latency_ms="${{ github.event.inputs.p95_latency_ms || '0' }}"
sample_size="${{ github.event.inputs.sample_size || '0' }}"
trigger_rollback_on_abort="${{ github.event.inputs.trigger_rollback_on_abort || 'true' }}"
rollback_branch="${{ github.event.inputs.rollback_branch || 'dev' }}"
rollback_target_ref="${{ github.event.inputs.rollback_target_ref || '' }}"
fail_on_violation="${{ github.event.inputs.fail_on_violation || 'true' }}"
else
git fetch --tags --force origin
candidate_tag="$(git tag --list 'v*' --sort=-version:refname | head -n1)"
if [ -n "$candidate_tag" ]; then
candidate_sha="$(git rev-parse "${candidate_tag}^{commit}")"
fi
fi
{
echo "mode=${mode}"
echo "candidate_tag=${candidate_tag}"
echo "candidate_sha=${candidate_sha}"
echo "error_rate=${error_rate}"
echo "crash_rate=${crash_rate}"
echo "p95_latency_ms=${p95_latency_ms}"
echo "sample_size=${sample_size}"
echo "trigger_rollback_on_abort=${trigger_rollback_on_abort}"
echo "rollback_branch=${rollback_branch}"
echo "rollback_target_ref=${rollback_target_ref}"
echo "fail_on_violation=${fail_on_violation}"
} >> "$GITHUB_OUTPUT"
- name: Run canary guard
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
args=()
if [ "${{ steps.inputs.outputs.fail_on_violation }}" = "true" ]; then
args+=(--fail-on-violation)
fi
python3 scripts/ci/canary_guard.py \
--policy-file .github/release/canary-policy.json \
--candidate-tag "${{ steps.inputs.outputs.candidate_tag }}" \
--candidate-sha "${{ steps.inputs.outputs.candidate_sha }}" \
--mode "${{ steps.inputs.outputs.mode }}" \
--error-rate "${{ steps.inputs.outputs.error_rate }}" \
--crash-rate "${{ steps.inputs.outputs.crash_rate }}" \
--p95-latency-ms "${{ steps.inputs.outputs.p95_latency_ms }}" \
--sample-size "${{ steps.inputs.outputs.sample_size }}" \
--output-json artifacts/canary-guard.json \
--output-md artifacts/canary-guard.md \
"${args[@]}"
- name: Extract canary decision outputs
id: extract
shell: bash
run: |
set -euo pipefail
decision="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/canary-guard.json', encoding='utf-8'))
print(data.get('decision', 'hold'))
PY
)"
ready_to_execute="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/canary-guard.json', encoding='utf-8'))
print(str(bool(data.get('ready_to_execute', False))).lower())
PY
)"
echo "decision=${decision}" >> "$GITHUB_OUTPUT"
echo "ready_to_execute=${ready_to_execute}" >> "$GITHUB_OUTPUT"
- name: Emit canary audit event
if: always()
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/emit_audit_event.py \
--event-type canary_guard \
--input-json artifacts/canary-guard.json \
--output-json artifacts/audit-event-canary-guard.json \
--artifact-name canary-guard \
--retention-days 21
- name: Publish canary summary
if: always()
shell: bash
run: |
set -euo pipefail
cat artifacts/canary-guard.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload canary artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: canary-guard
path: |
artifacts/canary-guard.json
artifacts/canary-guard.md
artifacts/audit-event-canary-guard.json
if-no-files-found: error
retention-days: 21
canary-execute:
name: Canary Execute
needs: [canary-plan]
if: github.event_name == 'workflow_dispatch' && needs.canary-plan.outputs.mode == 'execute' && needs.canary-plan.outputs.ready_to_execute == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 10
permissions:
contents: write
actions: write
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Create canary marker tag
shell: bash
run: |
set -euo pipefail
marker_tag="canary-${{ needs.canary-plan.outputs.candidate_tag }}-${{ github.run_id }}"
git fetch --tags --force origin
git tag -a "$marker_tag" "${{ needs.canary-plan.outputs.candidate_sha }}" -m "Canary decision marker from run ${{ github.run_id }}"
git push origin "$marker_tag"
echo "Created marker tag: $marker_tag" >> "$GITHUB_STEP_SUMMARY"
- name: Emit canary repository dispatch
if: github.event.inputs.emit_repository_dispatch == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.repos.createDispatchEvent({
owner: context.repo.owner,
repo: context.repo.repo,
event_type: `canary_${{ needs.canary-plan.outputs.decision }}`,
client_payload: {
candidate_tag: "${{ needs.canary-plan.outputs.candidate_tag }}",
candidate_sha: "${{ needs.canary-plan.outputs.candidate_sha }}",
decision: "${{ needs.canary-plan.outputs.decision }}",
run_id: context.runId,
run_attempt: process.env.GITHUB_RUN_ATTEMPT,
source_sha: context.sha
}
});
- name: Trigger rollback guard workflow on abort
if: needs.canary-plan.outputs.decision == 'abort' && needs.canary-plan.outputs.trigger_rollback_on_abort == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const rollbackBranch = "${{ needs.canary-plan.outputs.rollback_branch }}" || "dev";
const rollbackTargetRef = `${{ needs.canary-plan.outputs.rollback_target_ref }}`.trim();
const workflowRef = process.env.GITHUB_REF_NAME || "dev";
const inputs = {
branch: rollbackBranch,
mode: "execute",
allow_non_ancestor: "false",
fail_on_violation: "true",
create_marker_tag: "true",
emit_repository_dispatch: "true",
};
if (rollbackTargetRef.length > 0) {
inputs.target_ref = rollbackTargetRef;
}
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: "ci-rollback.yml",
ref: workflowRef,
inputs,
});
- name: Publish rollback trigger summary
if: needs.canary-plan.outputs.decision == 'abort'
shell: bash
run: |
set -euo pipefail
if [ "${{ needs.canary-plan.outputs.trigger_rollback_on_abort }}" = "true" ]; then
{
echo "### Canary Abort Rollback Trigger"
echo "- CI Rollback Guard dispatch: triggered"
echo "- Rollback branch: \`${{ needs.canary-plan.outputs.rollback_branch }}\`"
if [ -n "${{ needs.canary-plan.outputs.rollback_target_ref }}" ]; then
echo "- Rollback target ref: \`${{ needs.canary-plan.outputs.rollback_target_ref }}\`"
else
echo "- Rollback target ref: _auto (latest release tag strategy)_"
fi
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "### Canary Abort Rollback Trigger"
echo "- CI Rollback Guard dispatch: skipped (trigger_rollback_on_abort=false)"
} >> "$GITHUB_STEP_SUMMARY"
fi

296
.github/workflows/ci-cd-security.yml vendored Normal file
View File

@ -0,0 +1,296 @@
name: CI/CD with Security Hardening
# Hard rule (branch + cadence policy):
# 1) Contributors branch from `dev` and open PRs into `dev`.
# 2) PRs into `main` are promotion PRs from `dev` (or explicit hotfix override).
# 3) Full CI/CD runs on merge/direct push to `main` and manual dispatch only.
# 3a) Main/manual build triggers are restricted to maintainers:
# `theonlyhennygod`, `jordanthejet`.
# 4) release published: run publish path on every release.
# Cost policy: no daily auto-release and no heavy PR-triggered release pipeline.
on:
workflow_dispatch:
release:
types: [published]
concurrency:
group: ci-cd-security-${{ github.event.pull_request.number || github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
authorize-main-build:
name: Access and Execution Gate
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
outputs:
run_pipeline: ${{ steps.gate.outputs.run_pipeline }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 1
- name: Enforce actor policy and skip rules
id: gate
shell: bash
run: |
set -euo pipefail
actor="${GITHUB_ACTOR}"
actor_lc="$(echo "${actor}" | tr '[:upper:]' '[:lower:]')"
event="${GITHUB_EVENT_NAME}"
allowed_humans_lc="theonlyhennygod,jordanthejet"
allowed_bot="github-actions[bot]"
run_pipeline="true"
if [[ "${event}" == "push" ]]; then
commit_msg="$(git log -1 --pretty=%B | tr -d '\r')"
if [[ "${commit_msg}" == *"[skip ci]"* ]]; then
run_pipeline="false"
echo "Skipping heavy pipeline because commit message includes [skip ci]."
fi
if [[ "${run_pipeline}" == "true" && ",${allowed_humans_lc}," != *",${actor_lc},"* ]]; then
echo "::error::Only maintainer actors (${allowed_humans_lc}) can trigger main build runs. Actor: ${actor}"
exit 1
fi
elif [[ "${event}" == "workflow_dispatch" ]]; then
if [[ ",${allowed_humans_lc}," != *",${actor_lc},"* ]]; then
echo "::error::Only maintainer actors (${allowed_humans_lc}) can run manual CI/CD dispatches. Actor: ${actor}"
exit 1
fi
elif [[ "${event}" == "release" ]]; then
if [[ ",${allowed_humans_lc}," != *",${actor_lc},"* && "${actor}" != "${allowed_bot}" ]]; then
echo "::error::Only maintainer actors (${allowed_humans_lc}) or ${allowed_bot} can trigger release build lanes. Actor: ${actor}"
exit 1
fi
fi
echo "run_pipeline=${run_pipeline}" >> "$GITHUB_OUTPUT"
build-and-test:
needs: authorize-main-build
if: needs.authorize-main-build.outputs.run_pipeline == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
components: clippy, rustfmt
- name: Ensure C toolchain for Rust builds
shell: bash
run: ./scripts/ci/ensure_cc.sh
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-cd-security-build
cache-bin: false
- name: Build
shell: bash
run: cargo build --locked --verbose --all-features
- name: Run tests
shell: bash
run: cargo test --locked --verbose --all-features
- name: Run benchmarks
shell: bash
run: cargo bench --locked --verbose
- name: Lint with Clippy
shell: bash
run: cargo clippy --locked --all-targets --all-features -- -D warnings
- name: Check formatting
shell: bash
run: cargo fmt -- --check
security-scans:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 60
needs: build-and-test
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure C toolchain for Rust builds
shell: bash
run: ./scripts/ci/ensure_cc.sh
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-cd-security-security
cache-bin: false
- name: Install cargo-audit
shell: bash
run: cargo install cargo-audit --locked --features=fix
- name: Install cargo-deny
shell: bash
run: cargo install cargo-deny --locked
- name: Dependency vulnerability audit
shell: bash
run: cargo audit --deny warnings
- name: Dependency license and security check
shell: bash
run: cargo deny check
- name: Install gitleaks
shell: bash
run: |
set -euo pipefail
bin_dir="${RUNNER_TEMP}/bin"
mkdir -p "${bin_dir}"
bash ./scripts/ci/install_gitleaks.sh "${bin_dir}"
echo "${bin_dir}" >> "$GITHUB_PATH"
- name: Scan for secrets
shell: bash
run: gitleaks detect --source=. --verbose --config=.gitleaks.toml
- name: Static analysis with Semgrep
uses: semgrep/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1
with:
config: auto
fuzz-testing:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 90
needs: build-and-test
strategy:
fail-fast: false
matrix:
target:
- fuzz_config_parse
- fuzz_tool_params
- fuzz_webhook_payload
- fuzz_provider_response
- fuzz_command_validation
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: nightly
components: llvm-tools-preview
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-cd-security-fuzz
cache-bin: false
- name: Run fuzz tests
shell: bash
run: |
set -euo pipefail
cargo install cargo-fuzz --locked
cargo +nightly fuzz run ${{ matrix.target }} -- -max_total_time=300 -max_len=4096
container-build-and-scan:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 45
needs: security-scans
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Blacksmith Docker builder
uses: useblacksmith/setup-docker-builder@ef12d5b165b596e3aa44ea8198d8fde563eab402 # v1
- name: Build Docker image
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
push: false
load: true
tags: ghcr.io/${{ github.repository }}:ci-security
- name: Scan Docker image for vulnerabilities
shell: bash
run: |
set -euo pipefail
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:0.58.2 image \
--exit-code 1 \
--no-progress \
--severity HIGH,CRITICAL \
ghcr.io/${{ github.repository }}:ci-security
publish:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 60
if: github.event_name == 'release'
needs:
- build-and-test
- security-scans
- fuzz-testing
- container-build-and-scan
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Blacksmith Docker builder
uses: useblacksmith/setup-docker-builder@ef12d5b165b596e3aa44ea8198d8fde563eab402 # v1
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Build and push Docker image
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }},ghcr.io/${{ github.repository }}:latest
build-args: |
ZEROCLAW_CARGO_ALL_FEATURES=true

View File

@ -1,154 +0,0 @@
name: CI/CD Change Audit
on:
pull_request:
branches: [dev, main]
paths:
- ".github/workflows/**"
- ".github/release/**"
- ".github/codeql/**"
- "scripts/ci/**"
- ".github/dependabot.yml"
- "deny.toml"
- ".gitleaks.toml"
push:
branches: [dev, main]
paths:
- ".github/workflows/**"
- ".github/release/**"
- ".github/codeql/**"
- "scripts/ci/**"
- ".github/dependabot.yml"
- "deny.toml"
- ".gitleaks.toml"
workflow_dispatch:
inputs:
base_sha:
description: "Optional base SHA (default: HEAD~1)"
required: false
default: ""
type: string
fail_on_policy:
description: "Fail when audit policy violations are found"
required: true
default: true
type: boolean
concurrency:
group: ci-change-audit-${{ github.event.pull_request.number || github.sha || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
audit:
name: CI Change Audit
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Resolve base/head commits
id: refs
shell: bash
run: |
set -euo pipefail
head_sha="$(git rev-parse HEAD)"
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
# For pull_request events, checkout uses refs/pull/*/merge; HEAD^1 is the
# effective base commit for this synthesized merge and avoids stale base.sha.
if git rev-parse --verify HEAD^1 >/dev/null 2>&1; then
base_sha="$(git rev-parse HEAD^1)"
else
base_sha="${{ github.event.pull_request.base.sha }}"
fi
elif [ "${GITHUB_EVENT_NAME}" = "push" ]; then
base_sha="${{ github.event.before }}"
else
base_sha="${{ github.event.inputs.base_sha || '' }}"
if [ -z "$base_sha" ]; then
base_sha="$(git rev-parse HEAD~1)"
fi
fi
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- name: Run CI helper script unit tests
shell: bash
run: |
set -euo pipefail
python3 -m unittest discover -s scripts/ci/tests -p 'test_*.py' -v
- name: Generate CI change audit
shell: bash
env:
BASE_SHA: ${{ steps.refs.outputs.base_sha }}
HEAD_SHA: ${{ steps.refs.outputs.head_sha }}
run: |
set -euo pipefail
mkdir -p artifacts
fail_on_policy="true"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
fail_on_policy="${{ github.event.inputs.fail_on_policy || 'true' }}"
fi
cmd=(python3 scripts/ci/ci_change_audit.py
--base-sha "$BASE_SHA"
--head-sha "$HEAD_SHA"
--output-json artifacts/ci-change-audit.json
--output-md artifacts/ci-change-audit.md)
if [ "$fail_on_policy" = "true" ]; then
cmd+=(--fail-on-violations)
fi
"${cmd[@]}"
- name: Emit normalized audit event
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/ci-change-audit.json ]; then
python3 scripts/ci/emit_audit_event.py \
--event-type ci_change_audit \
--input-json artifacts/ci-change-audit.json \
--output-json artifacts/audit-event-ci-change-audit.json \
--artifact-name ci-change-audit-event \
--retention-days 14
fi
- name: Upload audit artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: ci-change-audit
path: artifacts/ci-change-audit.*
retention-days: 14
- name: Publish audit summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/ci-change-audit.md ]; then
cat artifacts/ci-change-audit.md >> "$GITHUB_STEP_SUMMARY"
else
echo "CI change audit report was not generated." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload audit event artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: ci-change-audit-event
path: artifacts/audit-event-ci-change-audit.json
if-no-files-found: ignore
retention-days: 14

View File

@ -1,112 +0,0 @@
name: CI Provider Connectivity
on:
schedule:
- cron: "30 */6 * * *" # Every 6 hours
workflow_dispatch:
inputs:
fail_on_critical:
description: "Fail run when critical endpoints are unreachable"
required: true
default: false
type: boolean
pull_request:
branches: [dev, main]
paths:
- ".github/workflows/ci-provider-connectivity.yml"
- ".github/connectivity/providers.json"
- "scripts/ci/provider_connectivity_matrix.py"
push:
branches: [dev, main]
paths:
- ".github/workflows/ci-provider-connectivity.yml"
- ".github/connectivity/providers.json"
- "scripts/ci/provider_connectivity_matrix.py"
concurrency:
group: provider-connectivity-${{ github.event.pull_request.number || github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
probe:
name: Provider Connectivity Probe
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Run connectivity matrix probe
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
fail_on_critical="false"
case "${GITHUB_EVENT_NAME}" in
schedule)
fail_on_critical="true"
;;
workflow_dispatch)
fail_on_critical="${{ github.event.inputs.fail_on_critical || 'false' }}"
;;
esac
cmd=(python3 scripts/ci/provider_connectivity_matrix.py
--config .github/connectivity/providers.json
--output-json artifacts/provider-connectivity-matrix.json
--output-md artifacts/provider-connectivity-matrix.md)
if [ "$fail_on_critical" = "true" ]; then
cmd+=(--fail-on-critical)
fi
"${cmd[@]}"
- name: Emit normalized audit event
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/provider-connectivity-matrix.json ]; then
python3 scripts/ci/emit_audit_event.py \
--event-type provider_connectivity \
--input-json artifacts/provider-connectivity-matrix.json \
--output-json artifacts/audit-event-provider-connectivity.json \
--artifact-name provider-connectivity-audit-event \
--retention-days 14
fi
- name: Upload connectivity artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: provider-connectivity-matrix
path: artifacts/provider-connectivity-matrix.*
retention-days: 14
- name: Publish summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/provider-connectivity-matrix.md ]; then
cat artifacts/provider-connectivity-matrix.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Provider connectivity report missing." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload audit event artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: provider-connectivity-audit-event
path: artifacts/audit-event-provider-connectivity.json
if-no-files-found: ignore
retention-days: 14

View File

@ -1,121 +0,0 @@
name: CI Reproducible Build
on:
push:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/reproducible_build_check.sh"
- ".github/workflows/ci-reproducible-build.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/reproducible_build_check.sh"
- ".github/workflows/ci-reproducible-build.yml"
schedule:
- cron: "45 5 * * 1" # Weekly Monday 05:45 UTC
workflow_dispatch:
inputs:
fail_on_drift:
description: "Fail workflow if deterministic hash drift is detected"
required: true
default: true
type: boolean
allow_build_id_drift:
description: "Treat GNU build-id-only drift as non-blocking"
required: true
default: true
type: boolean
concurrency:
group: repro-build-${{ github.event.pull_request.number || github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
reproducibility:
name: Reproducible Build Probe
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Run reproducible build check
shell: bash
run: |
set -euo pipefail
fail_on_drift="false"
allow_build_id_drift="true"
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
fail_on_drift="true"
elif [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
fail_on_drift="${{ github.event.inputs.fail_on_drift || 'true' }}"
allow_build_id_drift="${{ github.event.inputs.allow_build_id_drift || 'true' }}"
fi
FAIL_ON_DRIFT="$fail_on_drift" \
ALLOW_BUILD_ID_DRIFT="$allow_build_id_drift" \
OUTPUT_DIR="artifacts" \
./scripts/ci/reproducible_build_check.sh
- name: Emit normalized audit event
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/reproducible-build.json ]; then
python3 scripts/ci/emit_audit_event.py \
--event-type reproducible_build \
--input-json artifacts/reproducible-build.json \
--output-json artifacts/audit-event-reproducible-build.json \
--artifact-name reproducible-build-audit-event \
--retention-days 14
fi
- name: Upload reproducibility artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: reproducible-build
path: artifacts/reproducible-build*
retention-days: 14
- name: Upload audit event artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: reproducible-build-audit-event
path: artifacts/audit-event-reproducible-build.json
if-no-files-found: ignore
retention-days: 14
- name: Publish summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/reproducible-build.md ]; then
cat artifacts/reproducible-build.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Reproducible build report missing." >> "$GITHUB_STEP_SUMMARY"
fi

View File

@ -1,257 +0,0 @@
name: CI Rollback Guard
on:
workflow_dispatch:
inputs:
branch:
description: "Integration branch this rollback targets"
required: true
default: dev
type: choice
options:
- dev
- main
mode:
description: "dry-run only plans; execute enables rollback marker/dispatch actions"
required: true
default: dry-run
type: choice
options:
- dry-run
- execute
target_ref:
description: "Optional explicit rollback target (tag/sha/ref). Empty = latest matching tag."
required: false
default: ""
type: string
allow_non_ancestor:
description: "Allow target not being ancestor of current head (warning-only)"
required: true
default: false
type: boolean
fail_on_violation:
description: "Fail workflow when guard violations are detected"
required: true
default: true
type: boolean
create_marker_tag:
description: "In execute mode, create and push rollback marker tag"
required: true
default: false
type: boolean
emit_repository_dispatch:
description: "In execute mode, emit repository_dispatch event `rollback_execute`"
required: true
default: false
type: boolean
schedule:
- cron: "15 7 * * 1" # Weekly Monday 07:15 UTC
concurrency:
group: ci-rollback-${{ github.event.inputs.branch || 'dev' }}
cancel-in-progress: false
permissions:
contents: read
actions: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
rollback-plan:
name: Rollback Guard Plan
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
outputs:
branch: ${{ steps.plan.outputs.branch }}
mode: ${{ steps.plan.outputs.mode }}
target_sha: ${{ steps.plan.outputs.target_sha }}
target_ref: ${{ steps.plan.outputs.target_ref }}
ready_to_execute: ${{ steps.plan.outputs.ready_to_execute }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.branch || 'dev' }}
- name: Build rollback plan
id: plan
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
branch_input="dev"
mode_input="dry-run"
target_ref_input=""
allow_non_ancestor="false"
fail_on_violation="true"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
branch_input="${{ github.event.inputs.branch || 'dev' }}"
mode_input="${{ github.event.inputs.mode || 'dry-run' }}"
target_ref_input="${{ github.event.inputs.target_ref || '' }}"
allow_non_ancestor="${{ github.event.inputs.allow_non_ancestor || 'false' }}"
fail_on_violation="${{ github.event.inputs.fail_on_violation || 'true' }}"
fi
cmd=(python3 scripts/ci/rollback_guard.py
--repo-root .
--branch "$branch_input"
--mode "$mode_input"
--strategy latest-release-tag
--tag-pattern "v*"
--output-json artifacts/rollback-plan.json
--output-md artifacts/rollback-plan.md)
if [ -n "$target_ref_input" ]; then
cmd+=(--target-ref "$target_ref_input")
fi
if [ "$allow_non_ancestor" = "true" ]; then
cmd+=(--allow-non-ancestor)
fi
if [ "$fail_on_violation" = "true" ]; then
cmd+=(--fail-on-violation)
fi
"${cmd[@]}"
target_sha="$(python3 - <<'PY'
import json
d = json.load(open("artifacts/rollback-plan.json", "r", encoding="utf-8"))
print(d.get("target_sha", ""))
PY
)"
target_ref="$(python3 - <<'PY'
import json
d = json.load(open("artifacts/rollback-plan.json", "r", encoding="utf-8"))
print(d.get("target_ref", ""))
PY
)"
ready_to_execute="$(python3 - <<'PY'
import json
d = json.load(open("artifacts/rollback-plan.json", "r", encoding="utf-8"))
print(str(d.get("ready_to_execute", False)).lower())
PY
)"
{
echo "branch=$branch_input"
echo "mode=$mode_input"
echo "target_sha=$target_sha"
echo "target_ref=$target_ref"
echo "ready_to_execute=$ready_to_execute"
} >> "$GITHUB_OUTPUT"
- name: Emit rollback audit event
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/rollback-plan.json ]; then
python3 scripts/ci/emit_audit_event.py \
--event-type rollback_guard \
--input-json artifacts/rollback-plan.json \
--output-json artifacts/audit-event-rollback-guard.json \
--artifact-name ci-rollback-plan \
--retention-days 21
fi
- name: Upload rollback artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ci-rollback-plan
path: |
artifacts/rollback-plan.*
artifacts/audit-event-rollback-guard.json
if-no-files-found: ignore
retention-days: 21
- name: Publish rollback summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/rollback-plan.md ]; then
cat artifacts/rollback-plan.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Rollback plan markdown report missing." >> "$GITHUB_STEP_SUMMARY"
fi
rollback-execute:
name: Rollback Execute Actions
needs: [rollback-plan]
if: github.event_name == 'workflow_dispatch' && needs.rollback-plan.outputs.mode == 'execute' && needs.rollback-plan.outputs.ready_to_execute == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 15
permissions:
contents: write
actions: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
ref: ${{ needs.rollback-plan.outputs.branch }}
- name: Fetch tags
shell: bash
run: |
set -euo pipefail
git fetch --tags --force origin
- name: Create rollback marker tag
id: marker
if: github.event.inputs.create_marker_tag == 'true'
shell: bash
run: |
set -euo pipefail
target_sha="${{ needs.rollback-plan.outputs.target_sha }}"
if [ -z "$target_sha" ]; then
echo "Rollback guard did not resolve target_sha."
exit 1
fi
marker_tag="rollback-${{ needs.rollback-plan.outputs.branch }}-${{ github.run_id }}"
git tag -a "$marker_tag" "$target_sha" -m "Rollback marker from run ${{ github.run_id }}"
git push origin "$marker_tag"
echo "marker_tag=$marker_tag" >> "$GITHUB_OUTPUT"
- name: Emit rollback repository dispatch
if: github.event.inputs.emit_repository_dispatch == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.repos.createDispatchEvent({
owner: context.repo.owner,
repo: context.repo.repo,
event_type: "rollback_execute",
client_payload: {
branch: "${{ needs.rollback-plan.outputs.branch }}",
target_ref: "${{ needs.rollback-plan.outputs.target_ref }}",
target_sha: "${{ needs.rollback-plan.outputs.target_sha }}",
run_id: context.runId,
run_attempt: process.env.GITHUB_RUN_ATTEMPT,
source_sha: context.sha
}
});
- name: Publish execute summary
if: always()
shell: bash
run: |
set -euo pipefail
{
echo "### Rollback Execute Actions"
echo "- Branch: \`${{ needs.rollback-plan.outputs.branch }}\`"
echo "- Target ref: \`${{ needs.rollback-plan.outputs.target_ref }}\`"
echo "- Target sha: \`${{ needs.rollback-plan.outputs.target_sha }}\`"
if [ -n "${{ steps.marker.outputs.marker_tag || '' }}" ]; then
echo "- Marker tag: \`${{ steps.marker.outputs.marker_tag }}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"

View File

@ -9,7 +9,7 @@ on:
branches: [dev, main]
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
group: ci-run-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.sha }}
cancel-in-progress: true
permissions:
@ -24,12 +24,13 @@ env:
jobs:
changes:
name: Detect Change Scope
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
outputs:
docs_only: ${{ steps.scope.outputs.docs_only }}
docs_changed: ${{ steps.scope.outputs.docs_changed }}
rust_changed: ${{ steps.scope.outputs.rust_changed }}
workflow_changed: ${{ steps.scope.outputs.workflow_changed }}
ci_cd_changed: ${{ steps.scope.outputs.ci_cd_changed }}
docs_files: ${{ steps.scope.outputs.docs_files }}
base_sha: ${{ steps.scope.outputs.base_sha }}
steps:
@ -50,18 +51,34 @@ jobs:
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 25
timeout-minutes: 75
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
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
components: rustfmt, clippy
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- 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
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-lint
prefix-key: ci-run-check
cache-bin: false
- name: Run rust quality gate
run: ./scripts/ci/rust_quality_gate.sh
- name: Run strict lint delta gate
@ -69,81 +86,135 @@ jobs:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
run: ./scripts/ci/rust_strict_delta_gate.sh
test:
name: Test
needs: [changes, lint]
if: needs.changes.outputs.rust_changed == 'true' && needs.lint.result == 'success'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
with:
prefix-key: ci-run-test
- name: Run tests
run: cargo test --locked --verbose
build:
name: Build (Smoke)
workspace-check:
name: Workspace Check
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
timeout-minutes: 45
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- 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
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-build
cache-targets: true
- name: Build binary (smoke check)
run: cargo build --profile release-fast --locked --verbose
- name: Check binary size
run: bash scripts/ci/check_binary_size.sh target/release-fast/zeroclaw
prefix-key: ci-run-workspace-check
cache-bin: false
- name: Check workspace
run: cargo check --workspace --locked
flake-probe:
name: Test Flake Retry Probe
needs: [changes, lint, test]
if: always() && needs.changes.outputs.rust_changed == 'true' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:full'))
package-check:
name: Package Check (${{ matrix.package }})
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
package: [zeroclaw-types, zeroclaw-core]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- 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
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-flake-probe
- name: Probe flaky failure via single retry
prefix-key: ci-run-package-check
cache-bin: false
- name: Check package
run: cargo check -p ${{ matrix.package }} --locked
test:
name: Test
needs: [changes]
if: 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
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- 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
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-check
cache-bin: false
- name: Run tests with flake detection
shell: bash
env:
INITIAL_TEST_RESULT: ${{ needs.test.result }}
BLOCK_ON_FLAKE: ${{ vars.CI_BLOCK_ON_FLAKE_SUSPECTED || 'false' }}
run: |
set -euo pipefail
mkdir -p artifacts
python3 scripts/ci/flake_retry_probe.py \
--initial-result "${INITIAL_TEST_RESULT}" \
--retry-command "cargo test --locked --verbose" \
--output-json artifacts/flake-probe.json \
--output-md artifacts/flake-probe.md \
--block-on-flake "${BLOCK_ON_FLAKE}"
toolchain_bin=""
if [ -n "${CARGO:-}" ]; then
toolchain_bin="$(dirname "${CARGO}")"
elif [ -n "${RUSTC:-}" ]; then
toolchain_bin="$(dirname "${RUSTC}")"
fi
if [ -n "${toolchain_bin}" ] && [ -d "${toolchain_bin}" ]; then
case ":$PATH:" in
*":${toolchain_bin}:"*) ;;
*) export PATH="${toolchain_bin}:$PATH" ;;
esac
fi
if cargo test --locked --verbose; then
echo '{"flake_suspected":false,"status":"success"}' > artifacts/flake-probe.json
exit 0
fi
echo "::warning::First test run failed. Retrying for flake detection..."
if cargo test --locked --verbose; then
echo '{"flake_suspected":true,"status":"flake"}' > artifacts/flake-probe.json
echo "::warning::Flake suspected — test passed on retry"
if [ "${BLOCK_ON_FLAKE}" = "true" ]; then
echo "BLOCK_ON_FLAKE is set; failing on suspected flake."
exit 1
fi
exit 0
fi
echo '{"flake_suspected":false,"status":"failure"}' > artifacts/flake-probe.json
exit 1
- name: Publish flake probe summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/flake-probe.md ]; then
cat artifacts/flake-probe.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Flake probe report missing." >> "$GITHUB_STEP_SUMMARY"
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'])")
{
echo "### Test Flake Probe"
echo "- Status: \`${status}\`"
echo "- Flake suspected: \`${flake}\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload flake probe artifact
if: always()
@ -154,11 +225,163 @@ jobs:
if-no-files-found: ignore
retention-days: 14
build:
name: Build (Smoke)
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 90
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
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- 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
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-build
cache-targets: true
cache-bin: false
- name: Build binary (smoke check)
env:
CARGO_BUILD_JOBS: 2
CI_SMOKE_BUILD_ATTEMPTS: 3
run: bash scripts/ci/smoke_build_retry.sh
- name: Check binary size
env:
BINARY_SIZE_HARD_LIMIT_MB: 28
BINARY_SIZE_ADVISORY_MB: 20
BINARY_SIZE_TARGET_MB: 5
run: bash scripts/ci/check_binary_size.sh target/release-fast/zeroclaw
cross-platform-vm:
name: Cross-Platform VM (${{ matrix.name }})
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 80
strategy:
fail-fast: false
matrix:
include:
- name: ubuntu-24.04
os: ubuntu-24.04
shell: bash
command: cargo test --locked --lib --bins --verbose
- name: ubuntu-22.04
os: ubuntu-22.04
shell: bash
command: cargo test --locked --lib --bins --verbose
- name: windows-2022
os: windows-2022
shell: pwsh
command: cargo check --workspace --locked --all-targets --verbose
- name: macos-14
os: macos-14
shell: bash
command: cargo test --locked --lib --bins --verbose
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: ci-run-cross-vm-${{ matrix.name }}
cache-bin: false
- name: Build and test on VM
shell: ${{ matrix.shell }}
run: ${{ matrix.command }}
linux-distro-container:
name: Linux Distro Container (${{ matrix.name }})
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- name: debian-bookworm
image: debian:bookworm-slim
- name: ubuntu-24.04
image: ubuntu:24.04
- name: fedora-41
image: fedora:41
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Cargo check inside distro container
shell: bash
run: |
set -euo pipefail
docker run --rm \
-e CARGO_TERM_COLOR=always \
-v "$PWD":/work \
-w /work \
"${{ matrix.image }}" \
/bin/bash -lc '
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y --no-install-recommends \
curl ca-certificates build-essential pkg-config libssl-dev git
elif command -v dnf >/dev/null 2>&1; then
dnf install -y \
curl ca-certificates gcc gcc-c++ make pkgconfig openssl-devel git tar xz
else
echo "Unsupported package manager in ${HOSTNAME:-container}" >&2
exit 1
fi
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain 1.92.0
. "$HOME/.cargo/env"
rustc --version
cargo --version
cargo check --workspace --locked --all-targets --verbose
'
docker-smoke:
name: Docker Container Smoke
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Build release container image
shell: bash
run: |
set -euo pipefail
docker build --target release --tag zeroclaw-ci:${{ github.sha }} .
- name: Run container smoke check
shell: bash
run: |
set -euo pipefail
docker run --rm zeroclaw-ci:${{ github.sha }} --version
docs-only:
name: Docs-Only Fast Path
needs: [changes]
if: needs.changes.outputs.docs_only == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Skip heavy jobs for docs-only change
run: echo "Docs-only change detected. Rust lint/test/build skipped."
@ -167,7 +390,7 @@ jobs:
name: Non-Rust Fast Path
needs: [changes]
if: needs.changes.outputs.docs_only != 'true' && needs.changes.outputs.rust_changed != 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Skip Rust jobs for non-Rust change scope
run: echo "No Rust-impacting files changed. Rust lint/test/build skipped."
@ -176,12 +399,16 @@ jobs:
name: Docs Quality
needs: [changes]
if: needs.changes.outputs.docs_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Setup Node.js for markdown lint
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Markdown lint (changed lines only)
env:
@ -212,7 +439,7 @@ jobs:
- name: Link check (offline, added links only)
if: steps.collect_links.outputs.count != '0'
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
fail: true
args: >-
@ -231,7 +458,7 @@ jobs:
name: Lint Feedback
if: github.event_name == 'pull_request'
needs: [changes, lint, docs-quality]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
contents: read
pull-requests: write
@ -253,55 +480,11 @@ jobs:
const script = require('./.github/workflows/scripts/lint_feedback.js');
await script({github, context, core});
workflow-owner-approval:
name: Workflow Owner Approval
needs: [changes]
if: github.event_name == 'pull_request' && needs.changes.outputs.workflow_changed == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Require owner approval for workflow file changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_OWNER_LOGINS: ${{ vars.WORKFLOW_OWNER_LOGINS }}
with:
script: |
const script = require('./.github/workflows/scripts/ci_workflow_owner_approval.js');
await script({ github, context, core });
human-review-approval:
name: Human Review Approval
needs: [changes]
if: github.event_name == 'pull_request'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Require at least one human approving review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
HUMAN_REVIEW_BOT_LOGINS: ${{ vars.HUMAN_REVIEW_BOT_LOGINS }}
with:
script: |
const script = require('./.github/workflows/scripts/ci_human_review_guard.js');
await script({ github, context, core });
license-file-owner-guard:
name: License File Owner Guard
needs: [changes]
if: github.event_name == 'pull_request'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
permissions:
contents: read
pull-requests: read
@ -318,8 +501,8 @@ jobs:
ci-required:
name: CI Required Gate
if: always()
needs: [changes, lint, test, build, flake-probe, docs-only, non-rust, docs-quality, lint-feedback, workflow-owner-approval, human-review-approval, license-file-owner-guard]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
needs: [changes, lint, workspace-check, package-check, test, build, 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
shell: bash
@ -327,120 +510,77 @@ jobs:
set -euo pipefail
event_name="${{ github.event_name }}"
base_ref="${{ github.base_ref }}"
head_ref="${{ github.head_ref }}"
rust_changed="${{ needs.changes.outputs.rust_changed }}"
docs_changed="${{ needs.changes.outputs.docs_changed }}"
workflow_changed="${{ needs.changes.outputs.workflow_changed }}"
docs_result="${{ needs.docs-quality.result }}"
workflow_owner_result="${{ needs.workflow-owner-approval.result }}"
human_review_result="${{ needs.human-review-approval.result }}"
license_owner_result="${{ needs.license-file-owner-guard.result }}"
if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then
echo "workflow_owner_approval=${workflow_owner_result}"
echo "human_review_approval=${human_review_result}"
echo "license_file_owner_guard=${license_owner_result}"
if [ "$event_name" = "pull_request" ] && [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
# --- Helper: enforce PR governance gates ---
check_pr_governance() {
if [ "$event_name" != "pull_request" ]; then return 0; fi
if [ "$base_ref" = "main" ] && [ "$head_ref" != "dev" ]; then
echo "Promotion policy violation: PRs to main must originate from dev. Found ${head_ref} -> ${base_ref}."
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$human_review_result" != "success" ]; then
echo "Human review approval guard did not pass."
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$license_owner_result" != "success" ]; then
if [ "$license_owner_result" != "success" ]; then
echo "License file owner guard did not pass."
exit 1
fi
}
check_docs_quality() {
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Docs-only change detected, but docs-quality did not pass."
echo "Docs changed but docs-quality did not pass."
exit 1
fi
}
# --- Docs-only fast path ---
if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then
check_pr_governance
check_docs_quality
echo "Docs-only fast path passed."
exit 0
fi
# --- Non-rust fast path ---
if [ "$rust_changed" != "true" ]; then
echo "rust_changed=false (non-rust fast path)"
echo "workflow_owner_approval=${workflow_owner_result}"
echo "human_review_approval=${human_review_result}"
echo "license_file_owner_guard=${license_owner_result}"
if [ "$event_name" = "pull_request" ] && [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$human_review_result" != "success" ]; then
echo "Human review approval guard did not pass."
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$license_owner_result" != "success" ]; then
echo "License file owner guard did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Non-rust change touched docs, but docs-quality did not pass."
exit 1
fi
check_pr_governance
check_docs_quality
echo "Non-rust fast path passed."
exit 0
fi
# --- Rust change path ---
lint_result="${{ needs.lint.result }}"
lint_strict_delta_result="${{ needs.lint.result }}"
workspace_check_result="${{ needs.workspace-check.result }}"
package_check_result="${{ needs.package-check.result }}"
test_result="${{ needs.test.result }}"
build_result="${{ needs.build.result }}"
flake_result="${{ needs.flake-probe.result }}"
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 }}"
echo "lint=${lint_result}"
echo "lint_strict_delta=${lint_strict_delta_result}"
echo "workspace-check=${workspace_check_result}"
echo "package-check=${package_check_result}"
echo "test=${test_result}"
echo "build=${build_result}"
echo "flake_probe=${flake_result}"
echo "cross-platform-vm=${cross_platform_vm_result}"
echo "linux-distro-container=${linux_distro_container_result}"
echo "docker-smoke=${docker_smoke_result}"
echo "docs=${docs_result}"
echo "workflow_owner_approval=${workflow_owner_result}"
echo "human_review_approval=${human_review_result}"
echo "license_file_owner_guard=${license_owner_result}"
if [ "$event_name" = "pull_request" ] && [ "$workflow_changed" = "true" ] && [ "$workflow_owner_result" != "success" ]; then
echo "Workflow files changed but workflow owner approval gate did not pass."
check_pr_governance
if [ "$lint_result" != "success" ] || [ "$workspace_check_result" != "success" ] || [ "$package_check_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ] || [ "$cross_platform_vm_result" != "success" ] || [ "$linux_distro_container_result" != "success" ] || [ "$docker_smoke_result" != "success" ]; then
echo "Required CI jobs did not pass: lint=${lint_result} workspace-check=${workspace_check_result} package-check=${package_check_result} test=${test_result} build=${build_result} cross-platform-vm=${cross_platform_vm_result} linux-distro-container=${linux_distro_container_result} docker-smoke=${docker_smoke_result}"
exit 1
fi
if [ "$event_name" = "pull_request" ] && [ "$human_review_result" != "success" ]; then
echo "Human review approval guard did not pass."
exit 1
fi
check_docs_quality
if [ "$event_name" = "pull_request" ] && [ "$license_owner_result" != "success" ]; then
echo "License file owner guard did not pass."
exit 1
fi
if [ "$event_name" = "pull_request" ]; then
if [ "$lint_result" != "success" ] || [ "$lint_strict_delta_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then
echo "Required PR CI jobs did not pass."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "PR changed docs, but docs-quality did not pass."
exit 1
fi
echo "PR required checks passed."
exit 0
fi
if [ "$lint_result" != "success" ] || [ "$lint_strict_delta_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then
echo "Required push CI jobs did not pass."
exit 1
fi
if [ "$flake_result" != "success" ]; then
echo "Flake probe did not pass under current blocking policy."
exit 1
fi
if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then
echo "Push changed docs, but docs-quality did not pass."
exit 1
fi
echo "Push required checks passed."
echo "All required checks passed."

View File

@ -1,110 +0,0 @@
name: CI Supply Chain Provenance
on:
push:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/generate_provenance.py"
- ".github/workflows/ci-supply-chain-provenance.yml"
workflow_dispatch:
schedule:
- cron: "20 6 * * 1" # Weekly Monday 06:20 UTC
concurrency:
group: supply-chain-provenance-${{ github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
provenance:
name: Build + Provenance Bundle
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 35
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Build release-fast artifact
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
host_target="$(rustc -vV | sed -n 's/^host: //p')"
cargo build --profile release-fast --locked --target "$host_target"
cp "target/${host_target}/release-fast/zeroclaw" "artifacts/zeroclaw-${host_target}"
sha256sum "artifacts/zeroclaw-${host_target}" > "artifacts/zeroclaw-${host_target}.sha256"
- name: Generate provenance statement
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
python3 scripts/ci/generate_provenance.py \
--artifact "artifacts/zeroclaw-${host_target}" \
--subject-name "zeroclaw-${host_target}" \
--output "artifacts/provenance-${host_target}.intoto.json"
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Sign provenance bundle
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
statement="artifacts/provenance-${host_target}.intoto.json"
cosign sign-blob --yes \
--bundle="${statement}.sigstore.json" \
--output-signature="${statement}.sig" \
--output-certificate="${statement}.pem" \
"${statement}"
- name: Emit normalized audit event
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
python3 scripts/ci/emit_audit_event.py \
--event-type supply_chain_provenance \
--input-json "artifacts/provenance-${host_target}.intoto.json" \
--output-json "artifacts/audit-event-supply-chain-provenance.json" \
--artifact-name supply-chain-provenance \
--retention-days 30
- name: Upload provenance artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: supply-chain-provenance
path: artifacts/*
retention-days: 30
- name: Publish summary
shell: bash
run: |
set -euo pipefail
host_target="$(rustc -vV | sed -n 's/^host: //p')"
{
echo "### Supply Chain Provenance"
echo "- Target: \`${host_target}\`"
echo "- Artifact: \`artifacts/zeroclaw-${host_target}\`"
echo "- Statement: \`artifacts/provenance-${host_target}.intoto.json\`"
echo "- Signature: \`artifacts/provenance-${host_target}.intoto.json.sig\`"
} >> "$GITHUB_STEP_SUMMARY"

View File

@ -1,56 +0,0 @@
name: Deploy Web to GitHub Pages
on:
push:
branches: [main]
paths:
- 'web/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: Install dependencies
working-directory: ./web
run: npm ci
- name: Build
working-directory: ./web
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d
- name: Upload artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa
with:
path: ./web/dist
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e

View File

@ -1,291 +0,0 @@
name: Docs Deploy
on:
pull_request:
branches: [dev, main]
paths:
- "docs/**"
- "README*.md"
- ".github/workflows/docs-deploy.yml"
- "scripts/ci/docs_quality_gate.sh"
- "scripts/ci/collect_changed_links.py"
- ".github/release/docs-deploy-policy.json"
- "scripts/ci/docs_deploy_guard.py"
push:
branches: [dev, main]
paths:
- "docs/**"
- "README*.md"
- ".github/workflows/docs-deploy.yml"
- "scripts/ci/docs_quality_gate.sh"
- "scripts/ci/collect_changed_links.py"
- ".github/release/docs-deploy-policy.json"
- "scripts/ci/docs_deploy_guard.py"
workflow_dispatch:
inputs:
deploy_target:
description: "preview uploads artifact only; production deploys to Pages"
required: true
default: preview
type: choice
options:
- preview
- production
preview_evidence_run_url:
description: "Required for manual production deploys when policy enforces preview promotion evidence"
required: false
default: ""
rollback_ref:
description: "Optional rollback source ref (tag/sha/ref) for manual production dispatch"
required: false
default: ""
concurrency:
group: docs-deploy-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
docs-quality:
name: Docs Quality Gate
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
outputs:
docs_files: ${{ steps.scope.outputs.docs_files }}
base_sha: ${{ steps.scope.outputs.base_sha }}
deploy_target: ${{ steps.deploy_guard.outputs.deploy_target }}
deploy_mode: ${{ steps.deploy_guard.outputs.deploy_mode }}
source_ref: ${{ steps.deploy_guard.outputs.source_ref }}
production_branch_ref: ${{ steps.deploy_guard.outputs.production_branch_ref }}
ready_to_deploy: ${{ steps.deploy_guard.outputs.ready_to_deploy }}
docs_preview_retention_days: ${{ steps.deploy_guard.outputs.docs_preview_retention_days }}
docs_guard_artifact_retention_days: ${{ steps.deploy_guard.outputs.docs_guard_artifact_retention_days }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Resolve docs diff scope
id: scope
shell: bash
run: |
set -euo pipefail
base_sha=""
docs_files=""
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
base_sha="${{ github.event.pull_request.base.sha }}"
docs_files="$(git diff --name-only "$base_sha" HEAD | awk '/\.md$|\.mdx$|^README/ {print}')"
elif [ "${GITHUB_EVENT_NAME}" = "push" ]; then
base_sha="${{ github.event.before }}"
if [ -n "$base_sha" ] && [ "$base_sha" != "0000000000000000000000000000000000000000" ]; then
docs_files="$(git diff --name-only "$base_sha" HEAD | awk '/\.md$|\.mdx$|^README/ {print}')"
fi
else
docs_files="$(git ls-files 'docs/**/*.md' 'README*.md')"
fi
{
echo "base_sha=${base_sha}"
echo "docs_files<<EOF"
printf '%s\n' "$docs_files"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Validate docs deploy contract
id: deploy_guard
shell: bash
env:
INPUT_DEPLOY_TARGET: ${{ github.event.inputs.deploy_target || '' }}
INPUT_PREVIEW_EVIDENCE_RUN_URL: ${{ github.event.inputs.preview_evidence_run_url || '' }}
INPUT_ROLLBACK_REF: ${{ github.event.inputs.rollback_ref || '' }}
run: |
set -euo pipefail
mkdir -p artifacts
python3 scripts/ci/docs_deploy_guard.py \
--repo-root "$PWD" \
--event-name "${GITHUB_EVENT_NAME}" \
--git-ref "${GITHUB_REF}" \
--git-sha "${GITHUB_SHA}" \
--input-deploy-target "${INPUT_DEPLOY_TARGET}" \
--input-preview-evidence-run-url "${INPUT_PREVIEW_EVIDENCE_RUN_URL}" \
--input-rollback-ref "${INPUT_ROLLBACK_REF}" \
--policy-file .github/release/docs-deploy-policy.json \
--output-json artifacts/docs-deploy-guard.json \
--output-md artifacts/docs-deploy-guard.md \
--github-output-file "$GITHUB_OUTPUT" \
--fail-on-violation
- name: Emit docs deploy guard audit event
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/docs-deploy-guard.json ]; then
python3 scripts/ci/emit_audit_event.py \
--event-type docs_deploy_guard \
--input-json artifacts/docs-deploy-guard.json \
--output-json artifacts/audit-event-docs-deploy-guard.json \
--artifact-name docs-deploy-guard \
--retention-days 21
fi
- name: Publish docs deploy guard summary
if: always()
shell: bash
run: |
set -euo pipefail
if [ -f artifacts/docs-deploy-guard.md ]; then
cat artifacts/docs-deploy-guard.md >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload docs deploy guard artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: docs-deploy-guard
path: |
artifacts/docs-deploy-guard.json
artifacts/docs-deploy-guard.md
artifacts/audit-event-docs-deploy-guard.json
if-no-files-found: ignore
retention-days: ${{ steps.deploy_guard.outputs.docs_guard_artifact_retention_days || 21 }}
- name: Markdown quality gate
env:
BASE_SHA: ${{ steps.scope.outputs.base_sha }}
DOCS_FILES: ${{ steps.scope.outputs.docs_files }}
run: ./scripts/ci/docs_quality_gate.sh
- name: Collect added links
id: links
if: github.event_name != 'workflow_dispatch'
shell: bash
env:
BASE_SHA: ${{ steps.scope.outputs.base_sha }}
DOCS_FILES: ${{ steps.scope.outputs.docs_files }}
run: |
set -euo pipefail
python3 ./scripts/ci/collect_changed_links.py \
--base "$BASE_SHA" \
--docs-files "$DOCS_FILES" \
--output .ci-added-links.txt
count=$(wc -l < .ci-added-links.txt | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"
- name: Link check (added links)
if: github.event_name != 'workflow_dispatch' && steps.links.outputs.count != '0'
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2
with:
fail: true
args: >-
--offline
--no-progress
--format detailed
.ci-added-links.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Skip link check (none added)
if: github.event_name != 'workflow_dispatch' && steps.links.outputs.count == '0'
run: echo "No added links detected in changed docs lines."
docs-preview:
name: Docs Preview Artifact
needs: [docs-quality]
if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_target == 'preview')
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Build preview bundle
shell: bash
run: |
set -euo pipefail
rm -rf site
mkdir -p site/docs
cp -R docs/. site/docs/
cp README.md site/README.md
cat > site/index.md <<'EOF'
# ZeroClaw Docs Preview
This preview bundle is produced by `.github/workflows/docs-deploy.yml`.
- [Repository README](./README.md)
- [Docs Home](./docs/README.md)
EOF
- name: Upload preview artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: docs-preview
path: site/**
if-no-files-found: error
retention-days: ${{ needs.docs-quality.outputs.docs_preview_retention_days || 14 }}
docs-deploy:
name: Deploy Docs to GitHub Pages
needs: [docs-quality]
if: needs.docs-quality.outputs.deploy_target == 'production' && needs.docs-quality.outputs.ready_to_deploy == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.docs-quality.outputs.source_ref }}
- name: Build deploy bundle
shell: bash
run: |
set -euo pipefail
rm -rf site
mkdir -p site/docs
cp -R docs/. site/docs/
cp README.md site/README.md
cat > site/index.md <<'EOF'
# ZeroClaw Documentation
This site is deployed automatically from `main` by `.github/workflows/docs-deploy.yml`.
- [Repository README](./README.md)
- [Docs Home](./docs/README.md)
EOF
- name: Publish deploy source summary
shell: bash
run: |
{
echo "## Docs Deploy Source"
echo "- Deploy mode: \`${{ needs.docs-quality.outputs.deploy_mode }}\`"
echo "- Source ref: \`${{ needs.docs-quality.outputs.source_ref }}\`"
echo "- Production branch ref: \`${{ needs.docs-quality.outputs.production_branch_ref }}\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Setup Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

View File

@ -1,382 +0,0 @@
name: Feature Matrix
on:
push:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "tests/**"
- "scripts/ci/nightly_matrix_report.py"
- ".github/release/nightly-owner-routing.json"
- ".github/workflows/feature-matrix.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "tests/**"
- "scripts/ci/nightly_matrix_report.py"
- ".github/release/nightly-owner-routing.json"
- ".github/workflows/feature-matrix.yml"
merge_group:
branches: [dev, main]
schedule:
- cron: "30 4 * * 1" # Weekly Monday 04:30 UTC
- cron: "15 3 * * *" # Daily 03:15 UTC (nightly profile)
workflow_dispatch:
inputs:
profile:
description: "compile = merge-gate matrix, nightly = integration-oriented lane commands"
required: true
default: compile
type: choice
options:
- compile
- nightly
fail_on_failure:
description: "Fail summary job when any lane fails"
required: true
default: true
type: boolean
concurrency:
group: feature-matrix-${{ github.event.pull_request.number || github.ref || github.run_id }}-${{ github.event.inputs.profile || 'auto' }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
resolve-profile:
name: Resolve Matrix Profile
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
outputs:
profile: ${{ steps.resolve.outputs.profile }}
lane_job_prefix: ${{ steps.resolve.outputs.lane_job_prefix }}
summary_job_name: ${{ steps.resolve.outputs.summary_job_name }}
lane_retention_days: ${{ steps.resolve.outputs.lane_retention_days }}
lane_timeout_minutes: ${{ steps.resolve.outputs.lane_timeout_minutes }}
max_attempts: ${{ steps.resolve.outputs.max_attempts }}
summary_artifact_name: ${{ steps.resolve.outputs.summary_artifact_name }}
summary_json_name: ${{ steps.resolve.outputs.summary_json_name }}
summary_md_name: ${{ steps.resolve.outputs.summary_md_name }}
lane_artifact_prefix: ${{ steps.resolve.outputs.lane_artifact_prefix }}
fail_on_failure: ${{ steps.resolve.outputs.fail_on_failure }}
collect_history: ${{ steps.resolve.outputs.collect_history }}
steps:
- name: Resolve effective profile
id: resolve
shell: bash
run: |
set -euo pipefail
profile="compile"
fail_on_failure="true"
lane_job_prefix="Matrix Lane"
summary_job_name="Feature Matrix Summary"
lane_retention_days="21"
lane_timeout_minutes="55"
max_attempts="1"
summary_artifact_name="feature-matrix-summary"
summary_json_name="feature-matrix-summary.json"
summary_md_name="feature-matrix-summary.md"
lane_artifact_prefix="feature-matrix"
collect_history="false"
if [ "${GITHUB_EVENT_NAME}" = "schedule" ] && [ "${{ github.event.schedule }}" = "15 3 * * *" ]; then
profile="nightly"
elif [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
profile="${{ github.event.inputs.profile || 'compile' }}"
fail_on_failure="${{ github.event.inputs.fail_on_failure || 'true' }}"
fi
if [ "$profile" = "nightly" ]; then
lane_job_prefix="Nightly Lane"
summary_job_name="Nightly Summary & Routing"
lane_retention_days="30"
lane_timeout_minutes="70"
max_attempts="2"
summary_artifact_name="nightly-all-features-summary"
summary_json_name="nightly-summary.json"
summary_md_name="nightly-summary.md"
lane_artifact_prefix="nightly-lane"
collect_history="true"
fi
{
echo "profile=${profile}"
echo "lane_job_prefix=${lane_job_prefix}"
echo "summary_job_name=${summary_job_name}"
echo "lane_retention_days=${lane_retention_days}"
echo "lane_timeout_minutes=${lane_timeout_minutes}"
echo "max_attempts=${max_attempts}"
echo "summary_artifact_name=${summary_artifact_name}"
echo "summary_json_name=${summary_json_name}"
echo "summary_md_name=${summary_md_name}"
echo "lane_artifact_prefix=${lane_artifact_prefix}"
echo "fail_on_failure=${fail_on_failure}"
echo "collect_history=${collect_history}"
} >> "$GITHUB_OUTPUT"
feature-check:
name: ${{ needs.resolve-profile.outputs.lane_job_prefix }} (${{ matrix.name }})
needs: [resolve-profile]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: ${{ fromJSON(needs.resolve-profile.outputs.lane_timeout_minutes) }}
strategy:
fail-fast: false
matrix:
include:
- name: default
compile_command: cargo check --locked
nightly_command: cargo test --locked --test agent_e2e --verbose
install_libudev: false
- name: whatsapp-web
compile_command: cargo check --locked --no-default-features --features whatsapp-web
nightly_command: cargo check --locked --no-default-features --features whatsapp-web --verbose
install_libudev: false
- name: browser-native
compile_command: cargo check --locked --no-default-features --features browser-native
nightly_command: cargo check --locked --no-default-features --features browser-native --verbose
install_libudev: false
- name: nightly-all-features
compile_command: cargo check --locked --all-features
nightly_command: cargo test --locked --all-features --test agent_e2e --verbose
install_libudev: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
with:
prefix-key: feature-matrix-${{ matrix.name }}
- name: Ensure Linux deps for all-features lane
if: matrix.install_libudev
shell: bash
run: |
set -euo pipefail
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libudev; then
echo "libudev development headers already available; skipping apt install."
exit 0
fi
echo "Installing missing libudev build dependencies..."
for attempt in 1 2 3; do
if sudo apt-get update -qq -o DPkg::Lock::Timeout=300 && \
sudo apt-get install -y --no-install-recommends --no-upgrade -o DPkg::Lock::Timeout=300 libudev-dev pkg-config; then
echo "Dependency installation succeeded on attempt ${attempt}."
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to install libudev-dev/pkg-config after ${attempt} attempts." >&2
exit 1
fi
echo "Dependency installation failed on attempt ${attempt}; retrying in 10s..."
sleep 10
done
- name: Run matrix lane command
id: lane
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
profile="${{ needs.resolve-profile.outputs.profile }}"
lane_command="${{ matrix.compile_command }}"
if [ "$profile" = "nightly" ]; then
lane_command="${{ matrix.nightly_command }}"
fi
max_attempts="${{ needs.resolve-profile.outputs.max_attempts }}"
attempt=1
status=1
started_at="$(date +%s)"
while [ "$attempt" -le "$max_attempts" ]; do
echo "Running lane command (attempt ${attempt}/${max_attempts}): ${lane_command}"
set +e
bash -lc "${lane_command}"
status=$?
set -e
if [ "$status" -eq 0 ]; then
break
fi
if [ "$attempt" -lt "$max_attempts" ]; then
sleep 5
fi
attempt="$((attempt + 1))"
done
finished_at="$(date +%s)"
duration="$((finished_at - started_at))"
lane_status="success"
if [ "$status" -ne 0 ]; then
lane_status="failure"
fi
cat > "artifacts/nightly-result-${{ matrix.name }}.json" <<EOF
{
"lane": "${{ matrix.name }}",
"mode": "${profile}",
"status": "${lane_status}",
"exit_code": ${status},
"duration_seconds": ${duration},
"command": "${lane_command}",
"attempts_used": ${attempt},
"max_attempts": ${max_attempts}
}
EOF
{
echo "### ${{ needs.resolve-profile.outputs.lane_job_prefix }}: ${{ matrix.name }}"
echo "- Profile: \`${profile}\`"
echo "- Command: \`${lane_command}\`"
echo "- Status: ${lane_status}"
echo "- Exit code: ${status}"
echo "- Duration (s): ${duration}"
echo "- Attempts: ${attempt}/${max_attempts}"
} >> "$GITHUB_STEP_SUMMARY"
echo "lane_status=${lane_status}" >> "$GITHUB_OUTPUT"
echo "lane_exit_code=${status}" >> "$GITHUB_OUTPUT"
- name: Upload lane report
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ needs.resolve-profile.outputs.lane_artifact_prefix }}-${{ matrix.name }}
path: artifacts/nightly-result-${{ matrix.name }}.json
if-no-files-found: error
retention-days: ${{ fromJSON(needs.resolve-profile.outputs.lane_retention_days) }}
- name: Enforce lane success
if: steps.lane.outputs.lane_status != 'success'
shell: bash
run: |
set -euo pipefail
code="${{ steps.lane.outputs.lane_exit_code }}"
if [[ "$code" =~ ^[0-9]+$ ]]; then
# shellcheck disable=SC2242
exit "$code"
fi
echo "Invalid lane exit code: $code" >&2
exit 1
summary:
name: ${{ needs.resolve-profile.outputs.summary_job_name }}
needs: [resolve-profile, feature-check]
if: always()
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Download lane reports
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: artifacts
- name: Collect recent nightly history
if: needs.resolve-profile.outputs.collect_history == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const fs = require("fs");
const path = require("path");
const workflowId = "feature-matrix.yml";
const owner = context.repo.owner;
const repo = context.repo.repo;
const events = ["schedule", "workflow_dispatch"];
let runs = [];
for (const event of events) {
const resp = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: workflowId,
branch: "dev",
event,
per_page: 20,
});
runs = runs.concat(resp.data.workflow_runs || []);
}
const currentRunId = context.runId;
runs = runs
.filter((run) => run.id !== currentRunId && run.status === "completed")
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 3)
.map((run) => ({
run_id: run.id,
url: run.html_url,
event: run.event,
conclusion: run.conclusion || "unknown",
created_at: run.created_at,
head_sha: run.head_sha,
display_title: run.display_title || "",
}));
fs.mkdirSync("artifacts", { recursive: true });
fs.writeFileSync(
path.join("artifacts", "nightly-history.json"),
`${JSON.stringify(runs, null, 2)}\n`,
{ encoding: "utf8" }
);
- name: Aggregate matrix summary
shell: bash
run: |
set -euo pipefail
args=(
--input-dir artifacts
--owners-file .github/release/nightly-owner-routing.json
--output-json "artifacts/${{ needs.resolve-profile.outputs.summary_json_name }}"
--output-md "artifacts/${{ needs.resolve-profile.outputs.summary_md_name }}"
)
if [ "${{ needs.resolve-profile.outputs.collect_history }}" = "true" ] && [ -f artifacts/nightly-history.json ]; then
args+=(--history-file artifacts/nightly-history.json)
fi
if [ "${{ needs.resolve-profile.outputs.fail_on_failure }}" = "true" ]; then
args+=(--fail-on-failure)
fi
python3 scripts/ci/nightly_matrix_report.py "${args[@]}"
- name: Publish summary
shell: bash
run: |
set -euo pipefail
cat "artifacts/${{ needs.resolve-profile.outputs.summary_md_name }}" >> "$GITHUB_STEP_SUMMARY"
- name: Upload summary artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ needs.resolve-profile.outputs.summary_artifact_name }}
path: |
artifacts/${{ needs.resolve-profile.outputs.summary_json_name }}
artifacts/${{ needs.resolve-profile.outputs.summary_md_name }}
artifacts/nightly-history.json
if-no-files-found: error
retention-days: ${{ fromJSON(needs.resolve-profile.outputs.lane_retention_days) }}

View File

@ -1,266 +0,0 @@
# Main Branch Delivery Flows
This document explains what runs when code is proposed to `dev`/`main`, merged to `main`, and released.
Use this with:
- [`docs/ci-map.md`](../../docs/ci-map.md)
- [`docs/pr-workflow.md`](../../docs/pr-workflow.md)
- [`docs/release-process.md`](../../docs/release-process.md)
## Event Summary
| Event | Main workflows |
| --- | --- |
| PR activity (`pull_request_target`) | `pr-intake-checks.yml`, `pr-labeler.yml`, `pr-auto-response.yml` |
| PR activity (`pull_request`) | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows |
| Push to `dev`/`main` | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows |
| Tag push (`v*`) | `pub-release.yml` publish mode, `pub-docker-img.yml` publish job |
| Scheduled/manual | `pub-release.yml` verification mode, `sec-codeql.yml`, `feature-matrix.yml`, `test-fuzz.yml`, `pr-check-stale.yml`, `pr-check-status.yml`, `sync-contributors.yml`, `test-benchmarks.yml`, `test-e2e.yml` |
## Runtime and Docker Matrix
Observed averages below are from recent completed runs (sampled from GitHub Actions on February 17, 2026). Values are directional, not SLA.
| Workflow | Typical trigger in main flow | Avg runtime | Docker build? | Docker run? | Docker push? |
| --- | --- | ---:| --- | --- | --- |
| `pr-intake-checks.yml` | PR open/update (`pull_request_target`) | 14.5s | No | No | No |
| `pr-labeler.yml` | PR open/update (`pull_request_target`) | 53.7s | No | No | No |
| `pr-auto-response.yml` | PR/issue automation | 24.3s | No | No | No |
| `ci-run.yml` | PR + push to `dev`/`main` | 74.7s | No | No | No |
| `sec-audit.yml` | PR + push to `dev`/`main` | 127.2s | No | No | No |
| `workflow-sanity.yml` | Workflow-file changes | 34.2s | No | No | No |
| `pr-label-policy-check.yml` | Label policy/automation changes | 14.7s | No | No | No |
| `pub-docker-img.yml` (`pull_request`) | Docker build-input PR changes | 240.4s | Yes | Yes | No |
| `pub-docker-img.yml` (`push`) | tag push `v*` | 139.9s | Yes | No | Yes |
| `pub-release.yml` | Tag push `v*` (publish) + manual/scheduled verification (no publish) | N/A in recent sample | No | No | No |
Notes:
1. `pub-docker-img.yml` is the only workflow in the main PR/push path that builds Docker images.
2. Container runtime verification (`docker run`) occurs in PR smoke only.
3. Container registry push occurs on tag pushes (`v*`) only.
4. `ci-run.yml` "Build (Smoke)" builds Rust binaries, not Docker images.
## Step-By-Step
### 1) PR from branch in this repository -> `dev`
1. Contributor opens or updates PR against `dev`.
2. `pull_request_target` automation runs (typical runtime):
- `pr-intake-checks.yml` posts intake warnings/errors.
- `pr-labeler.yml` sets size/risk/scope labels.
- `pr-auto-response.yml` runs first-interaction and label routes.
3. `pull_request` CI workflows start:
- `ci-run.yml`
- `feature-matrix.yml` (Rust/workflow path scope)
- `sec-audit.yml`
- `sec-codeql.yml` (if Rust/codeql paths changed)
- path-scoped workflows if matching files changed:
- `pub-docker-img.yml` (Docker build-input paths only)
- `docs-deploy.yml` (docs + README markdown paths; deploy contract guard enforces promotion + rollback ref policy)
- `workflow-sanity.yml` (workflow files only)
- `pr-label-policy-check.yml` (label-policy files only)
- `ci-change-audit.yml` (CI/security path changes)
- `ci-provider-connectivity.yml` (probe config/script/workflow changes)
- `ci-reproducible-build.yml` (Rust/build reproducibility paths)
4. In `ci-run.yml`, `changes` computes:
- `docs_only`
- `docs_changed`
- `rust_changed`
- `workflow_changed`
5. `build` runs for Rust-impacting changes.
6. On PRs, full lint/test/docs checks run when PR has label `ci:full`:
- `lint`
- `lint-strict-delta`
- `test`
- `flake-probe` (single-retry telemetry; optional block via `CI_BLOCK_ON_FLAKE_SUSPECTED`)
- `docs-quality`
7. If `.github/workflows/**` changed, `workflow-owner-approval` must pass.
8. If root license files (`LICENSE-APACHE`, `LICENSE-MIT`) changed, `license-file-owner-guard` allows only PR author `willsarg`.
9. `lint-feedback` posts actionable comment if lint/docs gates fail.
10. `CI Required Gate` aggregates results to final pass/fail.
11. Maintainer merges PR once checks and review policy are satisfied.
12. Merge emits a `push` event on `dev` (see scenario 4).
### 2) PR from fork -> `dev`
1. External contributor opens PR from `fork/<branch>` into `zeroclaw:dev`.
2. Immediately on `opened`:
- `pull_request_target` workflows start with base-repo context and base-repo token:
- `pr-intake-checks.yml`
- `pr-labeler.yml`
- `pr-auto-response.yml`
- `pull_request` workflows are queued for the fork head commit:
- `ci-run.yml`
- `sec-audit.yml`
- path-scoped workflows (`pub-docker-img.yml`, `workflow-sanity.yml`, `pr-label-policy-check.yml`) if changed files match.
3. Fork-specific permission behavior in `pull_request` workflows:
- token is restricted (read-focused), so jobs that try to write PR comments/status extras can be limited.
- secrets from the base repo are not exposed to fork PR `pull_request` jobs.
4. Approval gate possibility:
- if Actions settings require maintainer approval for fork workflows, the `pull_request` run stays in `action_required`/waiting state until approved.
5. Event fan-out after labeling:
- `pr-labeler.yml` and manual label changes emit `labeled`/`unlabeled` events.
- those events retrigger `pull_request_target` automation (`pr-labeler.yml` and `pr-auto-response.yml`), creating extra run volume/noise.
6. When contributor pushes new commits to fork branch (`synchronize`):
- reruns: `pr-intake-checks.yml`, `pr-labeler.yml`, `ci-run.yml`, `sec-audit.yml`, and matching path-scoped PR workflows.
- does not rerun `pr-auto-response.yml` unless label/open events occur.
7. `ci-run.yml` execution details for fork PR:
- `changes` computes `docs_only`, `docs_changed`, `rust_changed`, `workflow_changed`.
- `build` runs for Rust-impacting changes.
- `lint`/`lint-strict-delta`/`test`/`docs-quality` run on PR when `ci:full` label exists.
- `workflow-owner-approval` runs when `.github/workflows/**` changed.
- `CI Required Gate` emits final pass/fail for the PR head.
8. Fork PR merge blockers to check first when diagnosing stalls:
- run approval pending for fork workflows.
- `workflow-owner-approval` failing on workflow-file changes.
- `license-file-owner-guard` failing when root license files are modified by non-owner PR author.
- `CI Required Gate` failure caused by upstream jobs.
- repeated `pull_request_target` reruns from label churn causing noisy signals.
9. After merge, normal `push` workflows on `dev` execute (scenario 4).
### 3) PR to `main` (direct or from `dev`)
1. Contributor or maintainer opens PR with base `main`.
2. `ci-run.yml` and `sec-audit.yml` run on the PR, plus any path-scoped workflows.
3. Maintainer merges PR once checks and review policy pass.
4. Merge emits a `push` event on `main`.
### 4) Push/Merge Queue to `dev` or `main` (including after merge)
1. Commit reaches `dev` or `main` (usually from a merged PR), or merge queue creates a `merge_group` validation commit.
2. `ci-run.yml` runs on `push` and `merge_group`.
3. `feature-matrix.yml` runs on `push` for Rust/workflow paths and on `merge_group`.
4. `sec-audit.yml` runs on `push` and `merge_group`.
5. `sec-codeql.yml` runs on `push`/`merge_group` when Rust/codeql paths change (path-scoped on push).
6. `ci-supply-chain-provenance.yml` runs on push when Rust/build provenance paths change.
7. Path-filtered workflows run only if touched files match their filters.
8. In `ci-run.yml`, push/merge-group behavior differs from PR behavior:
- Rust path: `lint`, `lint-strict-delta`, `test`, `build` are expected.
- Docs/non-rust paths: fast-path behavior applies.
9. `CI Required Gate` computes overall push/merge-group result.
## Docker Publish Logic
Workflow: `.github/workflows/pub-docker-img.yml`
### PR behavior
1. Triggered on `pull_request` to `dev` or `main` when Docker build-input paths change.
2. Runs `PR Docker Smoke` job:
- Builds local smoke image with Blacksmith builder.
- Verifies container with `docker run ... --version`.
3. Typical runtime in recent sample: ~240.4s.
4. No registry push happens on PR events.
### Push behavior
1. `publish` job runs on tag pushes `v*` only.
2. Workflow trigger includes semantic version tag pushes (`v*`) only.
3. Login to `ghcr.io` uses `${{ github.actor }}` and `${{ secrets.GITHUB_TOKEN }}`.
4. Tag computation includes semantic tag from pushed git tag (`vX.Y.Z`) + SHA tag (`sha-<12>`) + `latest`.
5. Multi-platform publish is used for tag pushes (`linux/amd64,linux/arm64`).
6. `scripts/ci/ghcr_publish_contract_guard.py` validates anonymous pullability and digest parity across `vX.Y.Z`, `sha-<12>`, and `latest`, then emits rollback candidate mapping evidence.
7. Trivy scans are emitted for version, SHA, and latest references.
8. `scripts/ci/ghcr_vulnerability_gate.py` validates Trivy JSON outputs against `.github/release/ghcr-vulnerability-policy.json` and emits audit-event evidence.
9. Typical runtime in recent sample: ~139.9s.
10. Result: pushed image tags under `ghcr.io/<owner>/<repo>` with publish-contract + vulnerability-gate + scan artifacts.
Important: Docker publish now requires a `v*` tag push; regular `dev`/`main` branch pushes do not publish images.
## Release Logic
Workflow: `.github/workflows/pub-release.yml`
1. Trigger modes:
- Tag push `v*` -> publish mode.
- Manual dispatch -> verification-only or publish mode (input-driven).
- Weekly schedule -> verification-only mode.
2. `prepare` resolves release context (`release_ref`, `release_tag`, publish/draft mode) and runs `scripts/ci/release_trigger_guard.py`.
- publish mode enforces actor authorization, stable annotated tag policy, `origin/main` ancestry, and `release_tag` == `Cargo.toml` version at the tag commit.
- trigger provenance is emitted as `release-trigger-guard` artifacts.
3. `build-release` builds matrix artifacts across Linux/macOS/Windows targets.
4. `verify-artifacts` runs `scripts/ci/release_artifact_guard.py` against `.github/release/release-artifact-contract.json` in verify-stage mode (archive contract required; manifest/SBOM/notice checks intentionally skipped) and uploads `release-artifact-guard-verify` evidence.
5. In publish mode, workflow generates SBOM (`CycloneDX` + `SPDX`), `SHA256SUMS`, and a checksum provenance statement (`zeroclaw.sha256sums.intoto.json`) plus audit-event envelope.
6. In publish mode, after manifest generation, workflow reruns `release_artifact_guard.py` in full-contract mode and emits `release-artifact-guard.publish.json` plus `audit-event-release-artifact-guard-publish.json`.
7. In publish mode, workflow keyless-signs release artifacts and composes a supply-chain release-notes preface via `release_notes_with_supply_chain_refs.py`.
8. In publish mode, workflow verifies GHCR release-tag availability.
9. In publish mode, workflow creates/updates the GitHub Release for the resolved tag and commit-ish, combining generated supply-chain preface with GitHub auto-generated commit notes.
Pre-release path:
1. Pre-release tags (`vX.Y.Z-alpha.N`, `vX.Y.Z-beta.N`, `vX.Y.Z-rc.N`) trigger `.github/workflows/pub-prerelease.yml`.
2. `scripts/ci/prerelease_guard.py` enforces stage progression, `origin/main` ancestry, and Cargo version/tag alignment.
3. In publish mode, prerelease assets are attached to a GitHub prerelease for the stage tag.
Canary policy lane:
1. `.github/workflows/ci-canary-gate.yml` runs weekly or manually.
2. `scripts/ci/canary_guard.py` evaluates metrics against `.github/release/canary-policy.json`.
3. Decision output is explicit (`promote`, `hold`, `abort`) with auditable artifacts and optional dispatch signal.
## Merge/Policy Notes
1. Workflow-file changes (`.github/workflows/**`) activate owner-approval gate in `ci-run.yml`.
2. PR lint/test strictness is intentionally controlled by `ci:full` label.
3. `pr-intake-checks.yml` now blocks PRs missing a Linear issue key (`RMN-*`, `CDV-*`, `COM-*`) to keep execution mapped to Linear.
4. `sec-audit.yml` runs on PR/push/merge queue (`merge_group`), plus scheduled weekly.
5. `ci-change-audit.yml` enforces pinned `uses:` references for CI/security workflow changes.
6. `sec-audit.yml` includes deny policy hygiene checks (`deny_policy_guard.py`) before cargo-deny.
7. `sec-audit.yml` includes gitleaks allowlist governance checks (`secrets_governance_guard.py`) against `.github/security/gitleaks-allowlist-governance.json`.
8. `ci-reproducible-build.yml` and `ci-supply-chain-provenance.yml` provide scheduled supply-chain assurance signals outside release-only windows.
9. Some workflows are operational and non-merge-path (`pr-check-stale`, `pr-check-status`, `sync-contributors`, etc.).
10. Workflow-specific JavaScript helpers are organized under `.github/workflows/scripts/`.
11. `ci-run.yml` includes cache partitioning (`prefix-key`) across lint/test/build/flake-probe lanes to reduce cache contention.
12. `ci-rollback.yml` provides a guarded rollback planning lane (scheduled dry-run + manual execute controls) with audit artifacts.
## Mermaid Diagrams
### PR to Dev
```mermaid
flowchart TD
A["PR opened or updated -> dev"] --> B["pull_request_target lane"]
B --> B1["pr-intake-checks.yml"]
B --> B2["pr-labeler.yml"]
B --> B3["pr-auto-response.yml"]
A --> C["pull_request CI lane"]
C --> C1["ci-run.yml"]
C --> C2["sec-audit.yml"]
C --> C3["pub-docker-img.yml (if Docker paths changed)"]
C --> C4["workflow-sanity.yml (if workflow files changed)"]
C --> C5["pr-label-policy-check.yml (if policy files changed)"]
C1 --> D["CI Required Gate"]
D --> E{"Checks + review policy pass?"}
E -->|No| F["PR stays open"]
E -->|Yes| G["Merge PR"]
G --> H["push event on dev"]
```
### Main Delivery and Release
```mermaid
flowchart TD
D0["Commit reaches dev"] --> B0["ci-run.yml"]
D0 --> C0["sec-audit.yml"]
PRM["PR to main"] --> QM["ci-run.yml + sec-audit.yml (+ path-scoped)"]
QM --> M["Merge to main"]
M --> A["Commit reaches main"]
A --> B["ci-run.yml"]
A --> C["sec-audit.yml"]
A --> D["path-scoped workflows (if matched)"]
T["Tag push v*"] --> R["pub-release.yml"]
W["Manual/Scheduled release verify"] --> R
T --> DP["pub-docker-img.yml publish job"]
R --> R1["Artifacts + SBOM + checksums + signatures + GitHub Release"]
W --> R2["Verification build only (no GitHub Release publish)"]
DP --> P1["Push ghcr image tags (version + sha + latest)"]
```
## Quick Troubleshooting
1. Unexpected skipped jobs: inspect `scripts/ci/detect_change_scope.sh` outputs.
2. Workflow-change PR blocked: verify `WORKFLOW_OWNER_LOGINS` and approvals.
3. Fork PR appears stalled: check whether Actions run approval is pending.
4. Docker not published: confirm a `v*` tag was pushed to the intended commit.

View File

@ -1,187 +0,0 @@
name: Nightly All-Features
on:
schedule:
- cron: "15 3 * * *" # Daily 03:15 UTC
workflow_dispatch:
inputs:
fail_on_failure:
description: "Fail workflow when any nightly lane fails"
required: true
default: true
type: boolean
concurrency:
group: nightly-all-features-${{ github.ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
nightly-lanes:
name: Nightly Lane (${{ matrix.name }})
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 70
strategy:
fail-fast: false
matrix:
include:
- name: default
command: cargo test --locked --test agent_e2e --verbose
install_libudev: false
- name: whatsapp-web
command: cargo check --locked --no-default-features --features whatsapp-web --verbose
install_libudev: false
- name: browser-native
command: cargo check --locked --no-default-features --features browser-native --verbose
install_libudev: false
- name: nightly-all-features
command: cargo test --locked --all-features --test agent_e2e --verbose
install_libudev: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
with:
prefix-key: nightly-all-features-${{ matrix.name }}
- name: Ensure Linux deps for all-features lane
if: matrix.install_libudev
shell: bash
run: |
set -euo pipefail
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libudev; then
echo "libudev development headers already available; skipping apt install."
exit 0
fi
echo "Installing missing libudev build dependencies..."
for attempt in 1 2 3; do
if sudo apt-get update -qq -o DPkg::Lock::Timeout=300 && \
sudo apt-get install -y --no-install-recommends --no-upgrade -o DPkg::Lock::Timeout=300 libudev-dev pkg-config; then
echo "Dependency installation succeeded on attempt ${attempt}."
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Failed to install libudev-dev/pkg-config after ${attempt} attempts." >&2
exit 1
fi
echo "Dependency installation failed on attempt ${attempt}; retrying in 10s..."
sleep 10
done
- name: Run nightly lane command
id: lane
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
started_at="$(date +%s)"
set +e
bash -lc "${{ matrix.command }}"
status=$?
set -e
finished_at="$(date +%s)"
duration="$((finished_at - started_at))"
lane_status="success"
if [ "$status" -ne 0 ]; then
lane_status="failure"
fi
cat > "artifacts/nightly-result-${{ matrix.name }}.json" <<EOF
{
"lane": "${{ matrix.name }}",
"status": "${lane_status}",
"exit_code": ${status},
"duration_seconds": ${duration},
"command": "${{ matrix.command }}"
}
EOF
{
echo "### Nightly Lane: ${{ matrix.name }}"
echo "- Command: \`${{ matrix.command }}\`"
echo "- Status: ${lane_status}"
echo "- Exit code: ${status}"
echo "- Duration (s): ${duration}"
} >> "$GITHUB_STEP_SUMMARY"
echo "lane_status=${lane_status}" >> "$GITHUB_OUTPUT"
echo "lane_exit_code=${status}" >> "$GITHUB_OUTPUT"
- name: Upload nightly lane artifact
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: nightly-lane-${{ matrix.name }}
path: artifacts/nightly-result-${{ matrix.name }}.json
if-no-files-found: error
retention-days: 30
nightly-summary:
name: Nightly Summary & Routing
needs: [nightly-lanes]
if: always()
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Download nightly artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: artifacts
- name: Aggregate nightly report
shell: bash
env:
FAIL_ON_FAILURE_INPUT: ${{ github.event.inputs.fail_on_failure || 'true' }}
run: |
set -euo pipefail
fail_on_failure="true"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
fail_on_failure="${FAIL_ON_FAILURE_INPUT}"
fi
args=()
if [ "$fail_on_failure" = "true" ]; then
args+=(--fail-on-failure)
fi
python3 scripts/ci/nightly_matrix_report.py \
--input-dir artifacts \
--owners-file .github/release/nightly-owner-routing.json \
--output-json artifacts/nightly-summary.json \
--output-md artifacts/nightly-summary.md \
"${args[@]}"
- name: Publish nightly summary
shell: bash
run: |
set -euo pipefail
cat artifacts/nightly-summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload nightly summary artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: nightly-all-features-summary
path: |
artifacts/nightly-summary.json
artifacts/nightly-summary.md
if-no-files-found: error
retention-days: 30

View File

@ -1,64 +0,0 @@
name: Deploy GitHub Pages
on:
push:
branches:
- main
paths:
- site/**
- docs/**
- README.md
- .github/workflows/pages-deploy.yml
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: github-pages
cancel-in-progress: true
jobs:
build:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: site/package-lock.json
- name: Install Dependencies
working-directory: site
run: npm ci
- name: Build Site
working-directory: site
run: npm run build
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload Artifact
uses: actions/upload-pages-artifact@v3
with:
path: gh-pages
deploy:
needs: build
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@ -1,89 +0,0 @@
name: PR Auto Responder
on:
issues:
types: [opened, reopened, labeled, unlabeled]
pull_request_target:
branches: [dev, main]
types: [opened, labeled, unlabeled]
permissions: {}
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
LABEL_POLICY_PATH: .github/label-policy.json
jobs:
contributor-tier-issues:
if: >-
(github.event_name == 'issues' &&
(github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')) ||
(github.event_name == 'pull_request_target' &&
(github.event.action == 'labeled' || github.event.action == 'unlabeled'))
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Apply contributor tier label for issue author
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
LABEL_POLICY_PATH: .github/label-policy.json
with:
script: |
const script = require('./.github/workflows/scripts/pr_auto_response_contributor_tier.js');
await script({ github, context, core });
first-interaction:
if: github.event.action == 'opened'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
permissions:
issues: write
pull-requests: write
steps:
- name: Greet first-time contributors
uses: actions/first-interaction@a1db7729b356323c7988c20ed6f0d33fe31297be # v1
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
issue_message: |
Thanks for opening this issue.
Before maintainers triage it, please confirm:
- Repro steps are complete and run on latest `main`
- Environment details are included (OS, Rust version, ZeroClaw version)
- Sensitive values are redacted
This helps us keep issue throughput high and response latency low.
pr_message: |
Thanks for contributing to ZeroClaw.
For faster review, please ensure:
- PR template sections are fully completed
- `cargo fmt --all -- --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` are included
- If automation/agents were used heavily, add brief workflow notes
- Scope is focused (prefer one concern per PR)
See `CONTRIBUTING.md` and `docs/pr-workflow.md` for full collaboration rules.
labeled-routes:
if: github.event.action == 'labeled'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Handle label-driven responses
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const script = require('./.github/workflows/scripts/pr_auto_response_labeled_routes.js');
await script({ github, context, core });

View File

@ -1,49 +0,0 @@
name: PR Check Stale
on:
schedule:
- cron: "20 2 * * *"
workflow_dispatch:
permissions: {}
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- name: Mark stale issues and pull requests
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 21
days-before-issue-close: 7
days-before-pr-stale: 14
days-before-pr-close: 7
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: security,pinned,no-stale,no-pr-hygiene,maintainer
exempt-pr-labels: no-stale,no-pr-hygiene,maintainer
remove-stale-when-updated: true
exempt-all-assignees: true
operations-per-run: 300
stale-issue-message: |
This issue was automatically marked as stale due to inactivity.
Please provide an update, reproduction details, or current status to keep it open.
close-issue-message: |
Closing this issue due to inactivity.
If the problem still exists on the latest `main`, please open a new issue with fresh repro steps.
close-issue-reason: not_planned
stale-pr-message: |
This PR was automatically marked as stale due to inactivity.
Please rebase/update and post the latest validation results.
close-pr-message: |
Closing this PR due to inactivity.
Maintainers can reopen once the branch is updated and validation is provided.

View File

@ -1,36 +0,0 @@
name: PR Check Status
on:
schedule:
- cron: "15 8 * * *" # Once daily at 8:15am UTC
workflow_dispatch:
permissions: {}
concurrency:
group: pr-check-status
cancel-in-progress: true
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
nudge-stale-prs:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
permissions:
contents: read
pull-requests: write
issues: write
env:
STALE_HOURS: "48"
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Nudge PRs that need rebase or CI refresh
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const script = require('./.github/workflows/scripts/pr_check_status_nudge.js');
await script({ github, context, core });

View File

@ -1,37 +0,0 @@
name: PR Intake Checks
on:
pull_request_target:
branches: [dev, main]
types: [opened, reopened, synchronize, edited, ready_for_review]
concurrency:
group: pr-intake-checks-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
intake:
name: Intake Checks
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Run safe PR intake checks
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const script = require('./.github/workflows/scripts/pr_intake_checks.js');
await script({ github, context, core });

View File

@ -1,80 +0,0 @@
name: PR Label Policy Check
on:
pull_request:
paths:
- ".github/label-policy.json"
- ".github/workflows/pr-labeler.yml"
- ".github/workflows/pr-auto-response.yml"
push:
paths:
- ".github/label-policy.json"
- ".github/workflows/pr-labeler.yml"
- ".github/workflows/pr-auto-response.yml"
concurrency:
group: pr-label-policy-check-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
contributor-tier-consistency:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Verify shared label policy and workflow wiring
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
import json
import re
from pathlib import Path
policy_path = Path('.github/label-policy.json')
policy = json.loads(policy_path.read_text(encoding='utf-8'))
color = str(policy.get('contributor_tier_color', '')).upper()
rules = policy.get('contributor_tiers', [])
if not re.fullmatch(r'[0-9A-F]{6}', color):
raise SystemExit('invalid contributor_tier_color in .github/label-policy.json')
if not rules:
raise SystemExit('contributor_tiers must not be empty in .github/label-policy.json')
labels = set()
prev_min = None
for entry in rules:
label = str(entry.get('label', '')).strip().lower()
min_merged = int(entry.get('min_merged_prs', 0))
if not label.endswith('contributor'):
raise SystemExit(f'invalid contributor tier label: {label}')
if label in labels:
raise SystemExit(f'duplicate contributor tier label: {label}')
if prev_min is not None and min_merged > prev_min:
raise SystemExit('contributor_tiers must be sorted descending by min_merged_prs')
labels.add(label)
prev_min = min_merged
workflow_paths = [
Path('.github/workflows/pr-labeler.yml'),
Path('.github/workflows/pr-auto-response.yml'),
]
for workflow in workflow_paths:
text = workflow.read_text(encoding='utf-8')
if '.github/label-policy.json' not in text:
raise SystemExit(f'{workflow} must load .github/label-policy.json')
if re.search(r'contributorTierColor\s*=\s*"[0-9A-Fa-f]{6}"', text):
raise SystemExit(f'{workflow} contains hardcoded contributorTierColor')
print('label policy file is valid and workflow consumers are wired to shared policy')
PY

View File

@ -1,56 +0,0 @@
name: PR Labeler
on:
pull_request_target:
branches: [dev, main]
types: [opened, reopened, synchronize, edited, labeled, unlabeled]
workflow_dispatch:
inputs:
mode:
description: "Run mode for managed-label governance"
required: true
default: "audit"
type: choice
options:
- audit
- repair
concurrency:
group: pr-labeler-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
LABEL_POLICY_PATH: .github/label-policy.json
jobs:
label:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Apply path labels
if: github.event_name == 'pull_request_target'
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
continue-on-error: true
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
sync-labels: true
- name: Apply size/risk/module labels
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
continue-on-error: true
env:
LABEL_POLICY_PATH: .github/label-policy.json
with:
script: |
const script = require('./.github/workflows/scripts/pr_labeler.js');
await script({ github, context, core });

View File

@ -17,6 +17,11 @@ on:
- "scripts/ci/ghcr_publish_contract_guard.py"
- "scripts/ci/ghcr_vulnerability_gate.py"
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to publish (e.g. v0.2.0). Leave empty for smoke-only run."
required: false
type: string
concurrency:
group: docker-${{ github.event.pull_request.number || github.ref }}
@ -32,7 +37,7 @@ env:
jobs:
pr-smoke:
name: PR Docker Smoke
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'workflow_dispatch' && inputs.release_tag == '')
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 25
permissions:
@ -41,6 +46,20 @@ jobs:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Resolve Docker API version
shell: bash
run: |
set -euo pipefail
server_api="$(docker version --format '{{.Server.APIVersion}}')"
min_api="$(docker version --format '{{.Server.MinAPIVersion}}' 2>/dev/null || true)"
if [[ -z "${server_api}" || "${server_api}" == "<no value>" ]]; then
echo "::error::Unable to detect Docker server API version."
docker version || true
exit 1
fi
echo "DOCKER_API_VERSION=${server_api}" >> "$GITHUB_ENV"
echo "Using Docker API version ${server_api} (server min: ${min_api:-unknown})"
- name: Setup Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
@ -72,9 +91,9 @@ jobs:
publish:
name: Build and Push Docker Image
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'zeroclaw-labs/zeroclaw'
if: github.repository == 'zeroclaw-labs/zeroclaw' && ((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && inputs.release_tag != ''))
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 45
timeout-minutes: 90
permissions:
contents: read
packages: write
@ -82,6 +101,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release_tag) || github.ref }}
- name: Resolve Docker API version
shell: bash
run: |
set -euo pipefail
server_api="$(docker version --format '{{.Server.APIVersion}}')"
min_api="$(docker version --format '{{.Server.MinAPIVersion}}' 2>/dev/null || true)"
if [[ -z "${server_api}" || "${server_api}" == "<no value>" ]]; then
echo "::error::Unable to detect Docker server API version."
docker version || true
exit 1
fi
echo "DOCKER_API_VERSION=${server_api}" >> "$GITHUB_ENV"
echo "Using Docker API version ${server_api} (server min: ${min_api:-unknown})"
- name: Setup Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
@ -99,22 +134,42 @@ jobs:
run: |
set -euo pipefail
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
SHA_SUFFIX="sha-${GITHUB_SHA::12}"
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Docker publish is restricted to v* tag pushes."
exit 1
fi
RELEASE_TAG="${GITHUB_REF#refs/tags/}"
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
RELEASE_TAG="${{ inputs.release_tag }}"
if [[ -z "${RELEASE_TAG}" ]]; then
echo "::error::workflow_dispatch publish requires inputs.release_tag"
exit 1
fi
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::release_tag must be vX.Y.Z or vX.Y.Z-suffix (received: ${RELEASE_TAG})"
exit 1
fi
if ! git rev-parse --verify "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
echo "::error::release tag not found in checkout: ${RELEASE_TAG}"
exit 1
fi
else
echo "::error::Unsupported event for publish: ${GITHUB_EVENT_NAME}"
exit 1
fi
RELEASE_SHA="$(git rev-parse HEAD)"
SHA_SUFFIX="sha-${RELEASE_SHA::12}"
SHA_TAG="${IMAGE}:${SHA_SUFFIX}"
LATEST_SUFFIX="latest"
LATEST_TAG="${IMAGE}:${LATEST_SUFFIX}"
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Docker publish is restricted to v* tag pushes."
exit 1
fi
RELEASE_TAG="${GITHUB_REF#refs/tags/}"
VERSION_TAG="${IMAGE}:${RELEASE_TAG}"
TAGS="${VERSION_TAG},${SHA_TAG},${LATEST_TAG}"
{
echo "tags=${TAGS}"
echo "release_tag=${RELEASE_TAG}"
echo "release_sha=${RELEASE_SHA}"
echo "sha_tag=${SHA_SUFFIX}"
echo "latest_tag=${LATEST_SUFFIX}"
} >> "$GITHUB_OUTPUT"
@ -124,6 +179,8 @@ jobs:
with:
context: .
push: true
build-args: |
ZEROCLAW_CARGO_ALL_FEATURES=true
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
@ -173,7 +230,7 @@ jobs:
python3 scripts/ci/ghcr_publish_contract_guard.py \
--repository "${GITHUB_REPOSITORY,,}" \
--release-tag "${{ steps.meta.outputs.release_tag }}" \
--sha "${GITHUB_SHA}" \
--sha "${{ steps.meta.outputs.release_sha }}" \
--policy-file .github/release/ghcr-tag-policy.json \
--output-json artifacts/ghcr-publish-contract.json \
--output-md artifacts/ghcr-publish-contract.md \
@ -328,11 +385,25 @@ jobs:
if-no-files-found: ignore
retention-days: 21
- name: Upload Trivy SARIF
- name: Detect Trivy SARIF report
id: trivy-sarif
if: always()
shell: bash
run: |
set -euo pipefail
sarif_path="artifacts/trivy-${{ steps.meta.outputs.release_tag }}.sarif"
if [ -f "${sarif_path}" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "::notice::Trivy SARIF report not found at ${sarif_path}; skipping SARIF upload."
fi
- name: Upload Trivy SARIF
if: always() && steps.trivy-sarif.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
sarif_file: artifacts/trivy-${{ github.ref_name }}.sarif
sarif_file: artifacts/trivy-${{ steps.meta.outputs.release_tag }}.sarif
category: ghcr-trivy
- name: Upload Trivy report artifacts
@ -341,9 +412,9 @@ jobs:
with:
name: ghcr-trivy-report
path: |
artifacts/trivy-${{ github.ref_name }}.sarif
artifacts/trivy-${{ github.ref_name }}.txt
artifacts/trivy-${{ github.ref_name }}.json
artifacts/trivy-${{ steps.meta.outputs.release_tag }}.sarif
artifacts/trivy-${{ steps.meta.outputs.release_tag }}.txt
artifacts/trivy-${{ steps.meta.outputs.release_tag }}.json
artifacts/trivy-sha-*.txt
artifacts/trivy-sha-*.json
artifacts/trivy-latest.txt

View File

@ -1,259 +0,0 @@
name: Pub Pre-release
on:
push:
tags:
- "v*-alpha.*"
- "v*-beta.*"
- "v*-rc.*"
workflow_dispatch:
inputs:
tag:
description: "Existing pre-release tag (e.g. v0.1.8-rc.1)"
required: true
default: ""
type: string
mode:
description: "dry-run validates/builds only; publish creates prerelease"
required: true
default: dry-run
type: choice
options:
- dry-run
- publish
draft:
description: "Create prerelease as draft"
required: true
default: true
type: boolean
concurrency:
group: prerelease-${{ github.ref || github.run_id }}
cancel-in-progress: false
permissions:
contents: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
prerelease-guard:
name: Pre-release Guard
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
outputs:
release_tag: ${{ steps.vars.outputs.release_tag }}
mode: ${{ steps.vars.outputs.mode }}
draft: ${{ steps.vars.outputs.draft }}
ready_to_publish: ${{ steps.extract.outputs.ready_to_publish }}
stage: ${{ steps.extract.outputs.stage }}
transition_outcome: ${{ steps.extract.outputs.transition_outcome }}
latest_stage: ${{ steps.extract.outputs.latest_stage }}
latest_stage_tag: ${{ steps.extract.outputs.latest_stage_tag }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Resolve prerelease inputs
id: vars
shell: bash
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
release_tag="${GITHUB_REF_NAME}"
mode="publish"
draft="false"
else
release_tag="${{ inputs.tag }}"
mode="${{ inputs.mode }}"
draft="${{ inputs.draft }}"
fi
{
echo "release_tag=${release_tag}"
echo "mode=${mode}"
echo "draft=${draft}"
} >> "$GITHUB_OUTPUT"
- name: Validate prerelease stage gate
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
python3 scripts/ci/prerelease_guard.py \
--repo-root . \
--tag "${{ steps.vars.outputs.release_tag }}" \
--stage-config-file .github/release/prerelease-stage-gates.json \
--mode "${{ steps.vars.outputs.mode }}" \
--output-json artifacts/prerelease-guard.json \
--output-md artifacts/prerelease-guard.md \
--fail-on-violation
- name: Extract prerelease outputs
id: extract
shell: bash
run: |
set -euo pipefail
ready_to_publish="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/prerelease-guard.json', encoding='utf-8'))
print(str(bool(data.get('ready_to_publish', False))).lower())
PY
)"
stage="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/prerelease-guard.json', encoding='utf-8'))
print(data.get('stage', 'unknown'))
PY
)"
transition_outcome="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/prerelease-guard.json', encoding='utf-8'))
transition = data.get('transition') or {}
print(transition.get('outcome', 'unknown'))
PY
)"
latest_stage="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/prerelease-guard.json', encoding='utf-8'))
history = data.get('stage_history') or {}
print(history.get('latest_stage', 'unknown'))
PY
)"
latest_stage_tag="$(python3 - <<'PY'
import json
data = json.load(open('artifacts/prerelease-guard.json', encoding='utf-8'))
history = data.get('stage_history') or {}
print(history.get('latest_tag', 'unknown'))
PY
)"
{
echo "ready_to_publish=${ready_to_publish}"
echo "stage=${stage}"
echo "transition_outcome=${transition_outcome}"
echo "latest_stage=${latest_stage}"
echo "latest_stage_tag=${latest_stage_tag}"
} >> "$GITHUB_OUTPUT"
- name: Emit prerelease audit event
if: always()
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/emit_audit_event.py \
--event-type prerelease_guard \
--input-json artifacts/prerelease-guard.json \
--output-json artifacts/audit-event-prerelease-guard.json \
--artifact-name prerelease-guard \
--retention-days 21
- name: Publish prerelease summary
if: always()
shell: bash
run: |
set -euo pipefail
cat artifacts/prerelease-guard.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload prerelease guard artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: prerelease-guard
path: |
artifacts/prerelease-guard.json
artifacts/prerelease-guard.md
artifacts/audit-event-prerelease-guard.json
if-no-files-found: error
retention-days: 21
build-prerelease:
name: Build Pre-release Artifact
needs: [prerelease-guard]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 45
steps:
- name: Checkout tag
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.prerelease-guard.outputs.release_tag }}
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
with:
prefix-key: prerelease-${{ needs.prerelease-guard.outputs.release_tag }}
cache-targets: true
- name: Build release-fast binary
shell: bash
run: |
set -euo pipefail
cargo build --profile release-fast --locked --target x86_64-unknown-linux-gnu
- name: Package prerelease artifact
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
cp target/x86_64-unknown-linux-gnu/release-fast/zeroclaw artifacts/zeroclaw
tar czf artifacts/zeroclaw-x86_64-unknown-linux-gnu.tar.gz -C artifacts zeroclaw
rm artifacts/zeroclaw
- name: Generate manifest + checksums
shell: bash
run: |
set -euo pipefail
python3 scripts/ci/release_manifest.py \
--artifacts-dir artifacts \
--release-tag "${{ needs.prerelease-guard.outputs.release_tag }}" \
--output-json artifacts/prerelease-manifest.json \
--output-md artifacts/prerelease-manifest.md \
--checksums-path artifacts/SHA256SUMS \
--fail-empty
- name: Publish prerelease build summary
shell: bash
run: |
set -euo pipefail
cat artifacts/prerelease-manifest.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload prerelease build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: prerelease-artifacts
path: artifacts/*
if-no-files-found: error
retention-days: 14
publish-prerelease:
name: Publish GitHub Pre-release
needs: [prerelease-guard, build-prerelease]
if: needs.prerelease-guard.outputs.ready_to_publish == 'true'
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 15
steps:
- name: Download prerelease artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: prerelease-artifacts
path: artifacts
- name: Create or update GitHub pre-release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
tag_name: ${{ needs.prerelease-guard.outputs.release_tag }}
prerelease: true
draft: ${{ needs.prerelease-guard.outputs.draft == 'true' }}
generate_release_notes: true
files: |
artifacts/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -25,9 +25,6 @@ on:
required: false
default: true
type: boolean
schedule:
# Weekly release-readiness verification on default branch (no publish)
- cron: "17 8 * * 1"
concurrency:
group: release-${{ github.ref || github.run_id }}
@ -47,6 +44,7 @@ env:
jobs:
prepare:
name: Prepare Release Context
if: github.event_name != 'push' || !contains(github.ref_name, '-')
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
outputs:
release_ref: ${{ steps.vars.outputs.release_ref }}
@ -106,7 +104,35 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install gh CLI
shell: bash
run: |
set -euo pipefail
if command -v gh &>/dev/null; then
echo "gh already available: $(gh --version | head -1)"
exit 0
fi
echo "Installing gh CLI..."
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
for i in {1..60}; do
if sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; then
echo "apt/dpkg locked; waiting ($i/60)..."
sleep 5
else
break
fi
done
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 update -qq
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y gh
env:
GH_TOKEN: ${{ github.token }}
- name: Validate release trigger and authorization guard
shell: bash
@ -121,12 +147,14 @@ jobs:
--release-ref "${{ steps.vars.outputs.release_ref }}" \
--release-tag "${{ steps.vars.outputs.release_tag }}" \
--publish-release "${{ steps.vars.outputs.publish_release }}" \
--authorized-actors "${{ vars.RELEASE_AUTHORIZED_ACTORS || 'willsarg,theonlyhennygod,chumyin' }}" \
--authorized-tagger-emails "${{ vars.RELEASE_AUTHORIZED_TAGGER_EMAILS || '' }}" \
--authorized-actors "${{ vars.RELEASE_AUTHORIZED_ACTORS || 'theonlyhennygod,JordanTheJet' }},github-actions[bot]" \
--authorized-tagger-emails "${{ vars.RELEASE_AUTHORIZED_TAGGER_EMAILS || '' }},41898282+github-actions[bot]@users.noreply.github.com" \
--require-annotated-tag true \
--output-json artifacts/release-trigger-guard.json \
--output-md artifacts/release-trigger-guard.md \
--fail-on-violation
env:
GH_TOKEN: ${{ github.token }}
- name: Emit release trigger audit event
if: always()
@ -164,6 +192,10 @@ jobs:
needs: [prepare]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
env:
CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ matrix.target }}/cargo
RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}-${{ matrix.target }}/rustup
CARGO_TARGET_DIR: ${{ github.workspace }}/target
strategy:
fail-fast: false
matrix:
@ -233,21 +265,21 @@ jobs:
linker_env: ""
linker: ""
use_cross: true
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
- os: macos-15-intel
target: x86_64-apple-darwin
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
- os: macos-14
target: aarch64-apple-darwin
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
- os: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: zeroclaw.exe
archive_ext: zip
@ -260,24 +292,52 @@ jobs:
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- 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
targets: ${{ matrix.target }}
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
if: runner.os != 'Windows'
- name: Install cross for cross-built targets
if: matrix.use_cross
shell: bash
run: |
cargo install cross --git https://github.com/cross-rs/cross
set -euo pipefail
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> "$GITHUB_PATH"
cargo install cross --locked --version 0.2.5
command -v cross
cross --version
- name: Install cross-compilation toolchain (Linux)
if: runner.os == 'Linux' && matrix.cross_compiler != ''
run: |
sudo apt-get update -qq
sudo apt-get install -y "${{ matrix.cross_compiler }}"
set -euo pipefail
for i in {1..60}; do
if sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; then
echo "apt/dpkg locked; waiting ($i/60)..."
sleep 5
else
break
fi
done
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 update -qq
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y "${{ matrix.cross_compiler }}"
# Install matching libc dev headers for cross targets
# (required by ring/aws-lc-sys C compilation)
case "${{ matrix.target }}" in
armv7-unknown-linux-gnueabihf)
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y libc6-dev-armhf-cross ;;
aarch64-unknown-linux-gnu)
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y libc6-dev-arm64-cross ;;
esac
- name: Setup Android NDK
if: matrix.android_ndk
@ -290,8 +350,18 @@ jobs:
NDK_ROOT="${RUNNER_TEMP}/android-ndk"
NDK_HOME="${NDK_ROOT}/android-ndk-${NDK_VERSION}"
sudo apt-get update -qq
sudo apt-get install -y unzip
for i in {1..60}; do
if sudo fuser /var/lib/apt/lists/lock >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; then
echo "apt/dpkg locked; waiting ($i/60)..."
sleep 5
else
break
fi
done
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 update -qq
sudo apt-get -o DPkg::Lock::Timeout=600 -o Acquire::Retries=3 install -y unzip
mkdir -p "${NDK_ROOT}"
curl -fsSL "${NDK_URL}" -o "${RUNNER_TEMP}/${NDK_ZIP}"
@ -362,6 +432,10 @@ jobs:
- name: Check binary size (Unix)
if: runner.os != 'Windows'
env:
BINARY_SIZE_HARD_LIMIT_MB: 28
BINARY_SIZE_ADVISORY_MB: 20
BINARY_SIZE_TARGET_MB: 5
run: bash scripts/ci/check_binary_size.sh "target/${{ matrix.target }}/release-fast/${{ matrix.artifact }}" "${{ matrix.target }}"
- name: Package (Unix)

View File

@ -15,6 +15,9 @@ on:
- ".github/security/unsafe-audit-governance.json"
- "scripts/ci/install_gitleaks.sh"
- "scripts/ci/install_syft.sh"
- "scripts/ci/ensure_c_toolchain.sh"
- "scripts/ci/ensure_cargo_component.sh"
- "scripts/ci/self_heal_rust_toolchain.sh"
- "scripts/ci/deny_policy_guard.py"
- "scripts/ci/secrets_governance_guard.py"
- "scripts/ci/unsafe_debt_audit.py"
@ -22,29 +25,12 @@ on:
- "scripts/ci/config/unsafe_debt_policy.toml"
- "scripts/ci/emit_audit_event.py"
- "scripts/ci/security_regression_tests.sh"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/sec-audit.yml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "deny.toml"
- ".gitleaks.toml"
- ".github/security/gitleaks-allowlist-governance.json"
- ".github/security/deny-ignore-governance.json"
- ".github/security/unsafe-audit-governance.json"
- "scripts/ci/install_gitleaks.sh"
- "scripts/ci/install_syft.sh"
- "scripts/ci/deny_policy_guard.py"
- "scripts/ci/secrets_governance_guard.py"
- "scripts/ci/unsafe_debt_audit.py"
- "scripts/ci/unsafe_policy_guard.py"
- "scripts/ci/config/unsafe_debt_policy.toml"
- "scripts/ci/emit_audit_event.py"
- "scripts/ci/security_regression_tests.sh"
- ".github/workflows/sec-audit.yml"
# Do not gate pull_request by paths: main branch protection requires
# "Security Required Gate" to always report a status on PRs.
merge_group:
branches: [dev, main]
schedule:
@ -87,13 +73,33 @@ jobs:
audit:
name: Security Audit
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
timeout-minutes: 45
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
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Self-heal Rust toolchain cache
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- 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
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0
with:
@ -103,9 +109,26 @@ jobs:
name: License & Supply Chain
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
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
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- name: Enforce deny policy hygiene
shell: bash
run: |
@ -118,9 +141,46 @@ jobs:
--output-md artifacts/deny-policy-guard.md \
--fail-on-violation
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
with:
command: check advisories licenses sources
- name: Install cargo-deny
shell: bash
run: |
set -euo pipefail
version="0.19.0"
arch="$(uname -m)"
case "${arch}" in
x86_64|amd64)
target="x86_64-unknown-linux-musl"
expected_sha256="0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f"
;;
aarch64|arm64)
target="aarch64-unknown-linux-musl"
expected_sha256="2b3567a60b7491c159d1cef8b7d8479d1ad2a31e29ef49462634ad4552fcc77d"
;;
*)
echo "Unsupported runner architecture for cargo-deny: ${arch}" >&2
exit 1
;;
esac
install_dir="${RUNNER_TEMP}/cargo-deny-${version}"
archive="${RUNNER_TEMP}/cargo-deny-${version}-${target}.tar.gz"
mkdir -p "${install_dir}"
curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \
--output "${archive}" \
"https://github.com/EmbarkStudios/cargo-deny/releases/download/${version}/cargo-deny-${version}-${target}.tar.gz"
actual_sha256="$(sha256sum "${archive}" | awk '{print $1}')"
if [ "${actual_sha256}" != "${expected_sha256}" ]; then
echo "Checksum mismatch for cargo-deny ${version} (${target})" >&2
echo "Expected: ${expected_sha256}" >&2
echo "Actual: ${actual_sha256}" >&2
exit 1
fi
tar -xzf "${archive}" -C "${install_dir}" --strip-components=1
echo "${install_dir}" >> "${GITHUB_PATH}"
"${install_dir}/cargo-deny" --version
- name: Run cargo-deny checks
shell: bash
run: cargo-deny check advisories licenses sources
- name: Emit deny audit event
if: always()
@ -158,21 +218,40 @@ jobs:
name: Security Regression Tests
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 30
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
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- 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
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: sec-audit-security-regressions
cache-bin: false
- name: Run security regression suite
shell: bash
run: ./scripts/ci/security_regression_tests.sh
secrets:
name: Secrets Governance (Gitleaks)
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -367,7 +446,7 @@ jobs:
sbom:
name: SBOM Snapshot
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@ -432,11 +511,17 @@ jobs:
unsafe-debt:
name: Unsafe Debt Audit
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Python 3.11
shell: bash
run: |
set -euo pipefail
python3 --version
- name: Enforce unsafe policy governance
shell: bash
run: |
@ -571,7 +656,7 @@ jobs:
name: Security Required Gate
if: always() && (github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group')
needs: [audit, deny, security-regressions, secrets, sbom, unsafe-debt]
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
steps:
- name: Enforce security gate
shell: bash

View File

@ -8,7 +8,11 @@ on:
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/ensure_c_toolchain.sh"
- "scripts/ci/ensure_cargo_component.sh"
- ".github/codeql/**"
- "scripts/ci/self_heal_rust_toolchain.sh"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/sec-codeql.yml"
pull_request:
branches: [dev, main]
@ -17,7 +21,11 @@ on:
- "Cargo.lock"
- "src/**"
- "crates/**"
- "scripts/ci/ensure_c_toolchain.sh"
- "scripts/ci/ensure_cargo_component.sh"
- ".github/codeql/**"
- "scripts/ci/self_heal_rust_toolchain.sh"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/sec-codeql.yml"
merge_group:
branches: [dev, main]
@ -41,16 +49,46 @@ env:
jobs:
select-runner:
name: Select CodeQL Runner Lane
runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40]
outputs:
labels: ${{ steps.lane.outputs.labels }}
lane: ${{ steps.lane.outputs.lane }}
steps:
- name: Resolve branch lane
id: lane
shell: bash
run: |
set -euo pipefail
branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
if [[ "$branch" == release/* ]]; then
echo 'labels=["self-hosted","Linux","X64","hetzner","codeql"]' >> "$GITHUB_OUTPUT"
echo 'lane=release' >> "$GITHUB_OUTPUT"
else
echo 'labels=["self-hosted","Linux","X64","hetzner","codeql","codeql-general"]' >> "$GITHUB_OUTPUT"
echo 'lane=general' >> "$GITHUB_OUTPUT"
fi
codeql:
name: CodeQL Analysis
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 30
needs: [select-runner]
runs-on: ${{ fromJSON(needs.select-runner.outputs.labels) }}
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
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Ensure C toolchain
shell: bash
run: bash ./scripts/ci/ensure_c_toolchain.sh
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
@ -59,10 +97,26 @@ jobs:
queries: security-and-quality
- name: Set up Rust
shell: bash
run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0
- name: Install Rust toolchain
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
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
with:
prefix-key: sec-codeql-build
cache-targets: true
cache-bin: false
- name: Build
run: cargo build --workspace --all-targets --locked
@ -70,3 +124,14 @@ jobs:
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
category: "/language:rust"
- name: Summarize lane
if: always()
shell: bash
run: |
{
echo "### CodeQL Runner Lane"
echo "- Branch: \`${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}\`"
echo "- Lane: \`${{ needs.select-runner.outputs.lane }}\`"
echo "- Labels: \`${{ needs.select-runner.outputs.labels }}\`"
} >> "$GITHUB_STEP_SUMMARY"

View File

@ -1,191 +0,0 @@
name: Sec Vorpal Reviewdog
on:
workflow_dispatch:
inputs:
scan_scope:
description: "File selection mode when source_path is empty"
required: true
type: choice
default: changed
options:
- changed
- all
base_ref:
description: "Base branch/ref for changed diff mode"
required: true
type: string
default: main
source_path:
description: "Optional comma-separated file paths to scan (overrides scan_scope)"
required: false
type: string
include_tests:
description: "Include test/fixture files in scan selection"
required: true
type: choice
default: "false"
options:
- "false"
- "true"
folders_to_ignore:
description: "Optional comma-separated path prefixes to ignore"
required: false
type: string
default: target,node_modules,web/dist,.venv,venv
reporter:
description: "Reviewdog reporter mode"
required: true
type: choice
default: github-pr-check
options:
- github-pr-check
- github-pr-review
filter_mode:
description: "Reviewdog filter mode"
required: true
type: choice
default: file
options:
- added
- diff_context
- file
- nofilter
level:
description: "Reviewdog severity level"
required: true
type: choice
default: error
options:
- info
- warning
- error
fail_on_error:
description: "Fail workflow when Vorpal reports findings"
required: true
type: choice
default: "false"
options:
- "false"
- "true"
reviewdog_flags:
description: "Optional extra reviewdog flags"
required: false
type: string
concurrency:
group: sec-vorpal-reviewdog-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
checks: write
pull-requests: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
vorpal:
name: Vorpal Reviewdog Scan
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Resolve source paths
id: sources
shell: bash
env:
INPUT_SOURCE_PATH: ${{ inputs.source_path }}
INPUT_SCAN_SCOPE: ${{ inputs.scan_scope }}
INPUT_BASE_REF: ${{ inputs.base_ref }}
INPUT_INCLUDE_TESTS: ${{ inputs.include_tests }}
run: |
set -euo pipefail
strip_space() {
local value="$1"
value="${value//$'\n'/}"
value="${value//$'\r'/}"
value="${value// /}"
echo "$value"
}
source_override="$(strip_space "${INPUT_SOURCE_PATH}")"
if [ -n "${source_override}" ]; then
normalized="$(echo "${INPUT_SOURCE_PATH}" | tr '\n' ',' | sed -E 's/[[:space:]]+//g; s/,+/,/g; s/^,|,$//g')"
if [ -n "${normalized}" ]; then
{
echo "scan=true"
echo "source_path=${normalized}"
echo "selection=manual"
} >> "${GITHUB_OUTPUT}"
exit 0
fi
fi
include_ext='\.(py|js|jsx|ts|tsx)$'
exclude_paths='^(target/|node_modules/|web/node_modules/|dist/|web/dist/|\.venv/|venv/)'
exclude_tests='(^|/)(test|tests|__tests__|fixtures|mocks|examples)/|(^|/)test_helpers/|(_test\.py$)|(^|/)test_.*\.py$|(\.spec\.(ts|tsx|js|jsx)$)|(\.test\.(ts|tsx|js|jsx)$)'
if [ "${INPUT_SCAN_SCOPE}" = "all" ]; then
candidate_files="$(git ls-files)"
else
base_ref="${INPUT_BASE_REF#refs/heads/}"
base_ref="${base_ref#origin/}"
if git fetch --no-tags --depth=1 origin "${base_ref}" >/dev/null 2>&1; then
if merge_base="$(git merge-base HEAD "origin/${base_ref}" 2>/dev/null)"; then
candidate_files="$(git diff --name-only --diff-filter=ACMR "${merge_base}"...HEAD)"
else
echo "Unable to resolve merge-base for origin/${base_ref}; falling back to tracked files."
candidate_files="$(git ls-files)"
fi
else
echo "Unable to fetch origin/${base_ref}; falling back to tracked files."
candidate_files="$(git ls-files)"
fi
fi
source_files="$(printf '%s\n' "${candidate_files}" | sed '/^$/d' | grep -E "${include_ext}" | grep -Ev "${exclude_paths}" || true)"
if [ "${INPUT_INCLUDE_TESTS}" != "true" ] && [ -n "${source_files}" ]; then
source_files="$(printf '%s\n' "${source_files}" | grep -Ev "${exclude_tests}" || true)"
fi
if [ -z "${source_files}" ]; then
{
echo "scan=false"
echo "source_path="
echo "selection=none"
} >> "${GITHUB_OUTPUT}"
exit 0
fi
source_path="$(printf '%s\n' "${source_files}" | paste -sd, -)"
{
echo "scan=true"
echo "source_path=${source_path}"
echo "selection=auto-${INPUT_SCAN_SCOPE}"
} >> "${GITHUB_OUTPUT}"
- name: No supported files to scan
if: steps.sources.outputs.scan != 'true'
shell: bash
run: |
echo "No supported files selected for Vorpal scan (extensions: .py .js .jsx .ts .tsx)."
- name: Run Vorpal with reviewdog
if: steps.sources.outputs.scan == 'true'
uses: Checkmarx/vorpal-reviewdog-github-action@8cc292f337a2f1dea581b4f4bd73852e7becb50d # v1.2.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
source_path: ${{ steps.sources.outputs.source_path }}
folders_to_ignore: ${{ inputs.folders_to_ignore }}
reporter: ${{ inputs.reporter }}
filter_mode: ${{ inputs.filter_mode }}
level: ${{ inputs.level }}
fail_on_error: ${{ inputs.fail_on_error }}
reviewdog_flags: ${{ inputs.reviewdog_flags }}

View File

@ -1,116 +0,0 @@
name: Sync Contributors
on:
workflow_dispatch:
schedule:
# Run every Sunday at 00:00 UTC
- cron: '0 0 * * 0'
concurrency:
group: update-notice-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
update-notice:
name: Update NOTICE with new contributors
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Fetch contributors
id: contributors
env:
GH_TOKEN: ${{ github.token }}
run: |
# Fetch all contributors (excluding bots)
gh api \
--paginate \
"repos/${{ github.repository }}/contributors" \
--jq '.[] | select(.type != "Bot") | .login' > /tmp/contributors_raw.txt
# Sort alphabetically and filter
sort -f < /tmp/contributors_raw.txt > contributors.txt
# Count contributors
count=$(wc -l < contributors.txt | tr -d ' ')
echo "count=$count" >> "$GITHUB_OUTPUT"
- name: Generate new NOTICE file
run: |
cat > NOTICE << 'EOF'
ZeroClaw
Copyright 2025 ZeroClaw Labs
This product includes software developed at ZeroClaw Labs (https://github.com/zeroclaw-labs).
Contributors
============
The following individuals have contributed to ZeroClaw:
EOF
# Append contributors in alphabetical order
sed 's/^/- /' contributors.txt >> NOTICE
# Add third-party dependencies section
cat >> NOTICE << 'EOF'
Third-Party Dependencies
=========================
This project uses the following third-party libraries and components,
each licensed under their respective terms:
See Cargo.lock for a complete list of dependencies and their licenses.
EOF
- name: Check if NOTICE changed
id: check_diff
run: |
if git diff --quiet NOTICE; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Create Pull Request
if: steps.check_diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
COUNT: ${{ steps.contributors.outputs.count }}
run: |
branch_name="auto/update-notice-$(date +%Y%m%d)"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch_name"
git add NOTICE
git commit -m "chore(notice): update contributor list"
git push origin "$branch_name"
gh pr create \
--title "chore(notice): update contributor list" \
--body "Auto-generated update to NOTICE file with $COUNT contributors." \
--label "chore" \
--label "docs" \
--draft || true
- name: Summary
run: |
echo "## NOTICE Update Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ steps.check_diff.outputs.changed }}" = "true" ]; then
echo "✅ PR created to update NOTICE" >> "$GITHUB_STEP_SUMMARY"
else
echo "✓ NOTICE file is up to date" >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Contributors:** ${{ steps.contributors.outputs.count }}" >> "$GITHUB_STEP_SUMMARY"

View File

@ -1,53 +0,0 @@
name: Test Benchmarks
on:
schedule:
- cron: "0 3 * * 1" # Weekly Monday 3am UTC
workflow_dispatch:
concurrency:
group: bench-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
benchmarks:
name: Criterion Benchmarks
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Run benchmarks
run: cargo bench --locked 2>&1 | tee benchmark_output.txt
- name: Upload benchmark results
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: benchmark-results
path: |
target/criterion/
benchmark_output.txt
retention-days: 7
- name: Post benchmark summary on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const script = require('./.github/workflows/scripts/test_benchmarks_pr_comment.js');
await script({ github, context, core });

View File

@ -3,10 +3,19 @@ name: Test E2E
on:
push:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "tests/**"
- "scripts/**"
- "scripts/ci/ensure_cc.sh"
- ".github/workflows/test-e2e.yml"
workflow_dispatch:
concurrency:
group: e2e-${{ github.event.pull_request.number || github.sha }}
group: test-e2e-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.sha }}
cancel-in-progress: true
permissions:
@ -28,6 +37,30 @@ jobs:
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Ensure cargo component
shell: bash
env:
ENSURE_CARGO_COMPONENT_STRICT: "true"
run: bash ./scripts/ci/ensure_cargo_component.sh 1.92.0
- name: Ensure C toolchain for Rust builds
run: ./scripts/ci/ensure_cc.sh
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3
- name: Runner preflight (compiler + disk)
shell: bash
run: |
set -euo pipefail
echo "Runner: ${RUNNER_NAME:-unknown} (${RUNNER_OS:-unknown}/${RUNNER_ARCH:-unknown})"
if ! command -v cc >/dev/null 2>&1; then
echo "::error::Missing 'cc' compiler on runner. Install build-essential (Debian/Ubuntu) or equivalent."
exit 1
fi
cc --version | head -n1
free_kb="$(df -Pk . | awk 'NR==2 {print $4}')"
min_kb=$((10 * 1024 * 1024))
if [ "${free_kb}" -lt "${min_kb}" ]; then
echo "::error::Insufficient disk space on runner (<10 GiB free)."
df -h .
exit 1
fi
- name: Run integration / E2E tests
run: cargo test --test agent_e2e --locked --verbose

View File

@ -1,75 +0,0 @@
name: Test Fuzz
on:
schedule:
- cron: "0 2 * * 0" # Weekly Sunday 2am UTC
workflow_dispatch:
inputs:
fuzz_seconds:
description: "Seconds to run each fuzz target"
required: false
default: "300"
concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
issues: write
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
CARGO_TERM_COLOR: always
jobs:
fuzz:
name: Fuzz (${{ matrix.target }})
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
target:
- fuzz_config_parse
- fuzz_tool_params
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: nightly
components: llvm-tools-preview
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Run fuzz target
run: |
SECONDS="${{ github.event.inputs.fuzz_seconds || '300' }}"
echo "Fuzzing ${{ matrix.target }} for ${SECONDS}s"
cargo +nightly fuzz run ${{ matrix.target }} -- \
-max_total_time="${SECONDS}" \
-max_len=4096
continue-on-error: true
id: fuzz
- name: Upload crash artifacts
if: failure() || steps.fuzz.outcome == 'failure'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: fuzz-crashes-${{ matrix.target }}
path: fuzz/artifacts/${{ matrix.target }}/
retention-days: 30
if-no-files-found: ignore
- name: Report fuzz results
run: |
echo "### Fuzz: ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ steps.fuzz.outcome }}" = "failure" ]; then
echo "- :x: Crashes found — see artifacts" >> "$GITHUB_STEP_SUMMARY"
else
echo "- :white_check_mark: No crashes found" >> "$GITHUB_STEP_SUMMARY"
fi

View File

@ -1,106 +0,0 @@
name: Workflow Sanity
on:
pull_request:
paths:
- ".github/workflows/**"
- ".github/*.yml"
- ".github/*.yaml"
push:
paths:
- ".github/workflows/**"
- ".github/*.yml"
- ".github/*.yaml"
concurrency:
group: workflow-sanity-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.hooksPath
GIT_CONFIG_VALUE_0: /dev/null
jobs:
no-tabs:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 10
steps:
- name: Normalize git global hooks config
shell: bash
run: |
set -euo pipefail
git config --global --unset-all core.hooksPath || true
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Fail on tabs in workflow files
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from __future__ import annotations
import pathlib
import sys
root = pathlib.Path(".github/workflows")
bad: list[str] = []
for path in sorted(root.rglob("*.yml")):
if b"\t" in path.read_bytes():
bad.append(str(path))
for path in sorted(root.rglob("*.yaml")):
if b"\t" in path.read_bytes():
bad.append(str(path))
if bad:
print("Tabs found in workflow file(s):")
for path in bad:
print(f"- {path}")
sys.exit(1)
PY
actionlint:
runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner]
timeout-minutes: 10
steps:
- name: Normalize git global hooks config
shell: bash
run: |
set -euo pipefail
git config --global --unset-all core.hooksPath || true
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install actionlint binary
shell: bash
run: |
set -euo pipefail
version="1.7.11"
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) archive="actionlint_${version}_linux_amd64.tar.gz" ;;
aarch64|arm64) archive="actionlint_${version}_linux_arm64.tar.gz" ;;
*)
echo "::error::Unsupported architecture: ${arch}"
exit 1
;;
esac
curl -fsSL \
-o "$RUNNER_TEMP/actionlint.tgz" \
"https://github.com/rhysd/actionlint/releases/download/v${version}/${archive}"
tar -xzf "$RUNNER_TEMP/actionlint.tgz" -C "$RUNNER_TEMP" actionlint
chmod +x "$RUNNER_TEMP/actionlint"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
"$RUNNER_TEMP/actionlint" -version
- name: Lint GitHub workflows
shell: bash
run: actionlint -color