ci: standardize production pipeline to 8 core workflows
This commit is contained in:
parent
b2b93ae861
commit
fdabb3c290
36
.github/workflows/README.md
vendored
36
.github/workflows/README.md
vendored
@ -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`
|
||||
169
.github/workflows/ci-auto-main-release.yml
vendored
Normal file
169
.github/workflows/ci-auto-main-release.yml
vendored
Normal 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
|
||||
63
.github/workflows/ci-build-fast.yml
vendored
63
.github/workflows/ci-build-fast.yml
vendored
@ -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
|
||||
329
.github/workflows/ci-canary-gate.yml
vendored
329
.github/workflows/ci-canary-gate.yml
vendored
@ -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
296
.github/workflows/ci-cd-security.yml
vendored
Normal 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
|
||||
154
.github/workflows/ci-change-audit.yml
vendored
154
.github/workflows/ci-change-audit.yml
vendored
@ -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
|
||||
112
.github/workflows/ci-provider-connectivity.yml
vendored
112
.github/workflows/ci-provider-connectivity.yml
vendored
@ -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
|
||||
121
.github/workflows/ci-reproducible-build.yml
vendored
121
.github/workflows/ci-reproducible-build.yml
vendored
@ -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
|
||||
257
.github/workflows/ci-rollback.yml
vendored
257
.github/workflows/ci-rollback.yml
vendored
@ -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"
|
||||
508
.github/workflows/ci-run.yml
vendored
508
.github/workflows/ci-run.yml
vendored
@ -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."
|
||||
|
||||
110
.github/workflows/ci-supply-chain-provenance.yml
vendored
110
.github/workflows/ci-supply-chain-provenance.yml
vendored
@ -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"
|
||||
56
.github/workflows/deploy-web.yml
vendored
56
.github/workflows/deploy-web.yml
vendored
@ -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
|
||||
291
.github/workflows/docs-deploy.yml
vendored
291
.github/workflows/docs-deploy.yml
vendored
@ -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
|
||||
382
.github/workflows/feature-matrix.yml
vendored
382
.github/workflows/feature-matrix.yml
vendored
@ -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) }}
|
||||
266
.github/workflows/main-branch-flow.md
vendored
266
.github/workflows/main-branch-flow.md
vendored
@ -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.
|
||||
187
.github/workflows/nightly-all-features.yml
vendored
187
.github/workflows/nightly-all-features.yml
vendored
@ -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
|
||||
64
.github/workflows/pages-deploy.yml
vendored
64
.github/workflows/pages-deploy.yml
vendored
@ -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
|
||||
89
.github/workflows/pr-auto-response.yml
vendored
89
.github/workflows/pr-auto-response.yml
vendored
@ -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 });
|
||||
49
.github/workflows/pr-check-stale.yml
vendored
49
.github/workflows/pr-check-stale.yml
vendored
@ -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.
|
||||
36
.github/workflows/pr-check-status.yml
vendored
36
.github/workflows/pr-check-status.yml
vendored
@ -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 });
|
||||
37
.github/workflows/pr-intake-checks.yml
vendored
37
.github/workflows/pr-intake-checks.yml
vendored
@ -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 });
|
||||
80
.github/workflows/pr-label-policy-check.yml
vendored
80
.github/workflows/pr-label-policy-check.yml
vendored
@ -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
|
||||
56
.github/workflows/pr-labeler.yml
vendored
56
.github/workflows/pr-labeler.yml
vendored
@ -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 });
|
||||
103
.github/workflows/pub-docker-img.yml
vendored
103
.github/workflows/pub-docker-img.yml
vendored
@ -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
|
||||
|
||||
259
.github/workflows/pub-prerelease.yml
vendored
259
.github/workflows/pub-prerelease.yml
vendored
@ -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 }}
|
||||
104
.github/workflows/pub-release.yml
vendored
104
.github/workflows/pub-release.yml
vendored
@ -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)
|
||||
|
||||
143
.github/workflows/sec-audit.yml
vendored
143
.github/workflows/sec-audit.yml
vendored
@ -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
|
||||
|
||||
69
.github/workflows/sec-codeql.yml
vendored
69
.github/workflows/sec-codeql.yml
vendored
@ -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"
|
||||
|
||||
191
.github/workflows/sec-vorpal-reviewdog.yml
vendored
191
.github/workflows/sec-vorpal-reviewdog.yml
vendored
@ -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 }}
|
||||
116
.github/workflows/sync-contributors.yml
vendored
116
.github/workflows/sync-contributors.yml
vendored
@ -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"
|
||||
53
.github/workflows/test-benchmarks.yml
vendored
53
.github/workflows/test-benchmarks.yml
vendored
@ -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 });
|
||||
37
.github/workflows/test-e2e.yml
vendored
37
.github/workflows/test-e2e.yml
vendored
@ -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
|
||||
|
||||
75
.github/workflows/test-fuzz.yml
vendored
75
.github/workflows/test-fuzz.yml
vendored
@ -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
|
||||
106
.github/workflows/workflow-sanity.yml
vendored
106
.github/workflows/workflow-sanity.yml
vendored
@ -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
|
||||
Loading…
Reference in New Issue
Block a user