Compare commits

...

29 Commits

Author SHA1 Message Date
jordanthejet 5dfd0a5e2b docs: update actions-source-policy for softprops removal and release changes
- Remove softprops/action-gh-release from allowlist (replaced by gh CLI)
- Update Beta Release trigger description (daily schedule + dispatch)
- Add changelog entry for release pipeline overhaul

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:38:15 -05:00
jordanthejet 2896875331 fix(ci): pin release tag to triggering commit via --target GITHUB_SHA
Without --target, gh release create tags the latest commit on the
default branch, which may differ from the commit that built the
artifacts. Pin to $GITHUB_SHA so the tag always matches the built ref.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:12:57 -05:00
jordanthejet a2d5672e72 fix(ci): correct artifact glob paths for release upload
SHA256SUMS is generated at artifacts/SHA256SUMS (not nested in a
subdirectory), so use artifacts/SHA256SUMS instead of artifacts/**/SHA256SUMS.
Also use artifacts/*/ single-level glob to match the download-artifact
directory structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:53:05 -05:00
jordanthejet bb7314bc37 fix(ci): add default-branch guard to beta release workflow_dispatch
Prevent manual beta releases from non-default branches by gating
the version job on github.ref matching the default branch. Off-branch
dispatches now fail closed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:52:40 -05:00
JordanTheJet b59e3ae6d1 Merge branch 'master' into fix/release-pipeline 2026-03-07 17:44:43 -05:00
jordanthejet 1f8934c4b4 feat(ci): add armv7 Linux build target to release pipelines
Add armv7-unknown-linux-gnueabihf to the build matrix for both beta
and stable release workflows, cross-compiled from ubuntu-latest using
gcc-arm-linux-gnueabihf.

Full release target matrix:
- Linux: x86_64, aarch64, armv7
- macOS: x86_64, aarch64
- Windows: x86_64

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:42:40 -05:00
jordanthejet ace9f19b11 fix(ci): overhaul release pipeline — daily betas, gh CLI publish, x86_64 macOS cross-compile
- Switch beta releases from per-merge to daily schedule (08:00 UTC)
  plus manual workflow_dispatch to reduce release spam
- Replace softprops/action-gh-release with gh CLI to fix the
  "Finalizing release" retry failure that left releases as drafts
- Beta releases use --prerelease; stable releases use --latest so
  the stable version stays prominently badged on the releases page
- Add x86_64-apple-darwin target back via cross-compile from macos-14
  (ARM runner), replacing the deprecated macos-13 Intel runner

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:46:37 -05:00
Argenis a6102f8dd6 Merge pull request #2928 from zeroclaw-labs/chore/master-branch-model
chore: migrate to single master branch model and update maintainers
2026-03-07 11:03:16 -05:00
jordanthejet b4d619dd2b fix: remove deprecated macos-13 x86_64-apple-darwin target from release pipelines
The macos-13 runner is deprecated by GitHub Actions, causing the
x86_64-apple-darwin build to instantly cancel with no runner assigned.
This cascades to skip publish and docker jobs since they depend on all
matrix builds succeeding.

Intel Macs have been EOL since 2022; aarch64-apple-darwin via macos-14
covers all current macOS users (Rosetta handles x86_64 if needed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:40:46 -05:00
jordanthejet 5dfe3372f5 fix: sync Vietnamese release-process with canonical English version
- Add missing Homebrew Core formula section (step 6)
- Add pub-homebrew-core.yml to workflow contract listing
- Update GHCR tag verification to include SHA tag detail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:18:56 -05:00
jordanthejet ce6349741b fix: sync Vietnamese ci-map with canonical English version
- Add missing Rust-gate parity line to CI merge-blocking section
- Add Sec Vorpal Reviewdog and Pub Homebrew Core workflow entries
- Add Pub Homebrew Core and Sec Vorpal Reviewdog to trigger map
- Update Dependabot trigger wording to match English (PRs target master)
- Add Homebrew formula triage entry and fix numbering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:13:40 -05:00
jordanthejet e3612880f3 fix: address remaining CodeRabbit review comments on PR #2928
- Fix Docker trigger semantics in Vietnamese ci-map docs to match
  canonical English wording (publish on tag push v*, smoke on master PRs)
- Add missing Rust-gate parity bullet to Vietnamese pr-workflow docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:07:45 -05:00
jordanthejet 73b862bb1f fix: address round-2 review comments on PR #2928
- docs/pr-workflow.md: replace hardcoded maintainer handles with
  generic WORKFLOW_OWNER_LOGINS + CODEOWNERS reference
- docs/vi/pr-workflow.md, docs/i18n/vi/pr-workflow.md: fix awkward
  "và hoặc là" phrasing, sync new branch-model bullets (workflow-owner
  config line + all PRs target master directly)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:20:44 -05:00
jordanthejet 31c027ed6d fix: replace remaining origin/main and main refs in release-process docs
- docs/release-process.md: 3 origin/main occurrences
- docs/vi/release-process.md: 3 origin/main + 6 `main` occurrences
- docs/i18n/vi/release-process.md: 3 origin/main + 6 `main` occurrences

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:53:03 -05:00
jordanthejet 571091ecef fix: resolve remaining main/dev references flagged in PR review
- docs/release-process.md: replace all `main` with `master` (lines 10,
  37, 45, 47, 54, 64, 73)
- docs/pr-workflow.md: fix "behind main" on line 219 to "behind master"
- docs/vi/pr-workflow.md: fix lines 216 and 273 (main -> master)
- docs/i18n/vi/pr-workflow.md: same fixes for canonical vi locale
- scripts/bootstrap.sh: fix raw URL from /main/ to /master/ (line 61),
  pin git clone to --branch master (line 909)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:37:36 -05:00
jordanthejet 44ac470d78 chore: migrate to single master branch model and update maintainers
- Replace all dev/main branch references with master across docs,
  templates, CI docs, and localized files (en, vi)
- Remove dev->main promotion model (no more Main Promotion Gate)
- Rename main-branch-flow.md to master-branch-flow.md and rewrite
  for single-branch workflow
- Update maintainers to theonlyhennygod and jordanthejet
- Update CODEOWNERS: replace @chumyin with @jordanthejet
- Update WORKFLOW_OWNER_LOGINS fallback references
- Update CODE_OF_CONDUCT enforcement contact to @argenistherose

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:32 -05:00
JordanTheJet 92e0f7aefd Merge pull request #2895 from zeroclaw-labs/sundai
ci: replace all workflows with simplified CI/CD pipeline
2026-03-05 22:52:53 -05:00
jordanthejet db536935bf fix: update coderabbit config for master branch and remove unsupported fields
- Changed base_branches from main/dev to master
- Removed unsupported base_branch_analysis field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:36:24 -05:00
JordanTheJet d64be99621 Update .github/workflows/ci-full.yml
macbook version update

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-05 22:35:07 -05:00
jordanthejet f981d9ea69 fix: address CodeRabbit review comments
- Add concurrency group to promote-release workflow
- Fix markdown emphasis style in README (MD049)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:26:29 -05:00
jordanthejet 8230b26171 fix(ci): remove sccache, keep mold + nextest + no-incremental
sccache GHA cache backend is fragile — fails the entire build when
GitHub's artifact cache service is unavailable. Removed in favor of
Swatinem/rust-cache which handles failures gracefully.

Kept: mold linker, cargo-nextest, CARGO_INCREMENTAL=0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:53:24 -05:00
jordanthejet 4f62fb2ecb ci: retrigger after allowlist update 2026-03-05 21:51:11 -05:00
jordanthejet fee163fc74 perf(ci): add sccache, mold linker, and cargo-nextest for faster CI
- sccache: compiler caching for test builds (11-14% faster compilation)
- mold: faster linker on Linux builds
- cargo-nextest: parallel test runner (up to 35-60% faster tests)
- CARGO_INCREMENTAL=0: disable incremental compilation overhead in CI

Allowlist impact: added mozilla-actions/sccache-action@*

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:50:07 -05:00
jordanthejet 32e8dbbec5 feat(ci): split CI into auto (linux+macOS) and manual full matrix
Auto CI on PRs builds linux x86_64 and macOS arm64 only.
Remaining targets (linux arm64, macOS x86, Windows) available via
manual workflow_dispatch in ci-full.yml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:43:33 -05:00
jordanthejet 6eef5bafcb feat(ci): full matrix build in CI + fix flaky bedrock test
- CI now builds across all 5 targets (linux x86/arm64, macOS x86/arm64,
  Windows) matching the release matrix
- Fix chat_fails_without_credentials test to accept "builder error"
  which occurs in CI environments without native TLS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:40:45 -05:00
jordanthejet 53c1a3ecea fix(ci): target master branch instead of main
The default branch is master, not main. Updates CI and Beta Release
workflow triggers and corresponding docs references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:34:33 -05:00
jordanthejet db917bc37b docs: update actions source policy for simplified workflow system
Reflects the complete workflow overhaul from 22 workflows to 3.
Updates allowlist to match current action usage and removes stale entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:24:42 -05:00
jordanthejet 47ea46e694 chore: restore .coderabbit.yaml config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:18:38 -05:00
jordanthejet 9923544769 ci: replace all workflows with simplified CI/CD pipeline
Remove 22 workflow files and 9 JS scripts. Replace with 3 workflows:
- ci.yml: test + build on PRs
- release.yml: auto beta release on merge to main
- promote-release.yml: manual stable release promotion

Update README Development section to document the new CI/CD system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:15:36 -05:00
58 changed files with 758 additions and 4257 deletions
+3 -7
View File
@@ -20,16 +20,12 @@ reviews:
enabled: true
# Only review PRs targeting these branches
base_branches:
- main
- develop
- master
# Skip reviews for draft PRs or WIP
drafts: false
# Enable base branch analysis
base_branch_analysis: true
# Poem configuration
poem:
enabled: false
# Poem feature toggle (must be a boolean, not an object)
poem: false
# Reviewer suggestions
reviewer:
+16 -16
View File
@@ -1,5 +1,5 @@
# Default owner for all files
* @chumyin
* @jordanthejet
# Important functional modules
/src/agent/** @theonlyhennygod
@@ -13,20 +13,20 @@
/Cargo.lock @theonlyhennygod
# Security / tests / CI-CD ownership
/src/security/** @chumyin
/tests/** @chumyin
/.github/** @chumyin
/.github/workflows/** @chumyin
/.github/codeql/** @chumyin
/.github/dependabot.yml @chumyin
/SECURITY.md @chumyin
/docs/actions-source-policy.md @chumyin
/docs/ci-map.md @chumyin
/src/security/** @jordanthejet
/tests/** @jordanthejet
/.github/** @jordanthejet
/.github/workflows/** @jordanthejet
/.github/codeql/** @jordanthejet
/.github/dependabot.yml @jordanthejet
/SECURITY.md @jordanthejet
/docs/actions-source-policy.md @jordanthejet
/docs/ci-map.md @jordanthejet
# Docs & governance
/docs/** @chumyin
/AGENTS.md @chumyin
/CLAUDE.md @chumyin
/CONTRIBUTING.md @chumyin
/docs/pr-workflow.md @chumyin
/docs/reviewer-playbook.md @chumyin
/docs/** @jordanthejet
/AGENTS.md @jordanthejet
/CLAUDE.md @jordanthejet
/CONTRIBUTING.md @jordanthejet
/docs/pr-workflow.md @jordanthejet
/docs/reviewer-playbook.md @jordanthejet
+1 -1
View File
@@ -140,7 +140,7 @@ body:
attributes:
label: Pre-flight checks
options:
- label: I reproduced this on the latest main branch or latest release.
- label: I reproduced this on the latest master branch or latest release.
required: true
- label: I redacted secrets/tokens from logs.
required: true
+2 -2
View File
@@ -4,8 +4,8 @@ contact_links:
url: https://github.com/zeroclaw-labs/zeroclaw/security/policy
about: Please report security vulnerabilities privately via SECURITY.md policy.
- name: Contribution guide
url: https://github.com/zeroclaw-labs/zeroclaw/blob/main/CONTRIBUTING.md
url: https://github.com/zeroclaw-labs/zeroclaw/blob/master/CONTRIBUTING.md
about: Please read contribution and PR requirements before opening an issue.
- name: PR workflow & reviewer expectations
url: https://github.com/zeroclaw-labs/zeroclaw/blob/main/docs/pr-workflow.md
url: https://github.com/zeroclaw-labs/zeroclaw/blob/master/docs/pr-workflow.md
about: Read risk-based PR tracks, CI gates, and merge criteria before filing feature requests.
+1 -1
View File
@@ -2,7 +2,7 @@
Describe this PR in 2-5 bullets:
- Base branch target (`dev` for normal contributions; `main` only for `dev` promotion):
- Base branch target (`master` for all contributions):
- Problem:
- Why it matters:
- What changed:
-61
View File
@@ -1,61 +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:
CARGO_TERM_COLOR: always
jobs:
changes:
name: Detect Change Scope
runs-on: blacksmith-2vcpu-ubuntu-2404
outputs:
rust_changed: ${{ steps.scope.outputs.rust_changed }}
docs_only: ${{ steps.scope.outputs.docs_only }}
workflow_changed: ${{ steps.scope.outputs.workflow_changed }}
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' || needs.changes.outputs.workflow_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
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
+52
View File
@@ -0,0 +1,52 @@
name: CI Full Matrix
on:
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
cross_compiler: gcc-aarch64-linux-gnu
linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
linker: aarch64-linux-gnu-gcc
- os: macos-15-intel
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
if: runner.os != 'Windows'
- name: Install cross compiler
if: matrix.cross_compiler
run: |
sudo apt-get update -qq
sudo apt-get install -y ${{ matrix.cross_compiler }}
- name: Build release
shell: bash
run: |
if [ -n "${{ matrix.linker_env || '' }}" ] && [ -n "${{ matrix.linker || '' }}" ]; then
export "${{ matrix.linker_env }}=${{ matrix.linker }}"
fi
cargo build --release --locked --target ${{ matrix.target }}
-340
View File
@@ -1,340 +0,0 @@
name: CI Run
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
changes:
name: Detect Change Scope
runs-on: blacksmith-2vcpu-ubuntu-2404
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 }}
docs_files: ${{ steps.scope.outputs.docs_files }}
base_sha: ${{ steps.scope.outputs.base_sha }}
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
lint:
name: Lint Gate (Format + Clippy + Strict Delta)
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
components: rustfmt, clippy
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Run rust quality gate
run: ./scripts/ci/rust_quality_gate.sh
- name: Run strict lint delta gate
env:
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: blacksmith-2vcpu-ubuntu-2404
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 tests
run: cargo test --locked --verbose
build:
name: Build (Smoke)
needs: [changes]
if: needs.changes.outputs.rust_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- 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
docs-only:
name: Docs-Only Fast Path
needs: [changes]
if: needs.changes.outputs.docs_only == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Skip heavy jobs for docs-only change
run: echo "Docs-only change detected. Rust lint/test/build skipped."
non-rust:
name: Non-Rust Fast Path
needs: [changes]
if: needs.changes.outputs.docs_only != 'true' && needs.changes.outputs.rust_changed != 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Skip Rust jobs for non-Rust change scope
run: echo "No Rust-impacting files changed. Rust lint/test/build skipped."
docs-quality:
name: Docs Quality
needs: [changes]
if: needs.changes.outputs.docs_changed == 'true'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Markdown lint (changed lines only)
env:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
DOCS_FILES: ${{ needs.changes.outputs.docs_files }}
run: ./scripts/ci/docs_quality_gate.sh
- name: Collect added links
id: collect_links
shell: bash
env:
BASE_SHA: ${{ needs.changes.outputs.base_sha }}
DOCS_FILES: ${{ needs.changes.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"
if [ "$count" -gt 0 ]; then
echo "Added links queued for check:"
cat .ci-added-links.txt
else
echo "No added links found in changed docs lines."
fi
- name: Link check (offline, added links only)
if: steps.collect_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 (no added links)
if: steps.collect_links.outputs.count == '0'
run: echo "No added links in changed docs lines. Link check skipped."
lint-feedback:
name: Lint Feedback
if: github.event_name == 'pull_request'
needs: [changes, lint, docs-quality]
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Post actionable lint failure summary
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
RUST_CHANGED: ${{ needs.changes.outputs.rust_changed }}
DOCS_CHANGED: ${{ needs.changes.outputs.docs_changed }}
LINT_RESULT: ${{ needs.lint.result }}
LINT_DELTA_RESULT: ${{ needs.lint.result }}
DOCS_RESULT: ${{ needs.docs-quality.result }}
with:
script: |
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: blacksmith-2vcpu-ubuntu-2404
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 });
license-file-owner-guard:
name: License File Owner Guard
needs: [changes]
if: github.event_name == 'pull_request'
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Enforce owner-only edits for root license files
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const script = require('./.github/workflows/scripts/ci_license_file_owner_guard.js');
await script({ github, context, core });
ci-required:
name: CI Required Gate
if: always()
needs: [changes, lint, test, build, docs-only, non-rust, docs-quality, lint-feedback, workflow-owner-approval, license-file-owner-guard]
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Enforce required status
shell: bash
run: |
set -euo pipefail
event_name="${{ github.event_name }}"
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 }}"
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 "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" ] && [ "$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 "Docs-only change detected, but docs-quality did not pass."
exit 1
fi
echo "Docs-only fast path passed."
exit 0
fi
if [ "$rust_changed" != "true" ]; then
echo "rust_changed=false (non-rust fast path)"
echo "workflow_owner_approval=${workflow_owner_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" ] && [ "$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
echo "Non-rust fast path passed."
exit 0
fi
lint_result="${{ needs.lint.result }}"
lint_strict_delta_result="${{ needs.lint.result }}"
test_result="${{ needs.test.result }}"
build_result="${{ needs.build.result }}"
echo "lint=${lint_result}"
echo "lint_strict_delta=${lint_strict_delta_result}"
echo "test=${test_result}"
echo "build=${build_result}"
echo "docs=${docs_result}"
echo "workflow_owner_approval=${workflow_owner_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" ] && [ "$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 [ "$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."
+75
View File
@@ -0,0 +1,75 @@
name: CI
on:
pull_request:
branches: [master]
concurrency:
group: ci-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
- uses: Swatinem/rust-cache@v2
- name: Install mold linker
run: |
sudo apt-get update -qq
sudo apt-get install -y mold
- name: Install cargo-nextest
run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
- name: Run tests
run: cargo nextest run --locked
env:
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C link-arg=-fuse-ld=mold"
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-14
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install mold linker
if: runner.os == 'Linux'
run: |
sudo apt-get update -qq
sudo apt-get install -y mold
- name: Build release
shell: bash
run: cargo build --release --locked --target ${{ matrix.target }}
env:
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: clang
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C link-arg=-fuse-ld=mold"
-57
View File
@@ -1,57 +0,0 @@
name: Feature Matrix
on:
schedule:
- cron: "30 4 * * 1" # Weekly Monday 4:30am UTC
workflow_dispatch:
concurrency:
group: feature-matrix-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
feature-check:
name: Check (${{ matrix.name }})
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: no-default-features
args: --no-default-features
install_libudev: false
- name: all-features
args: --all-features
install_libudev: true
- name: hardware-only
args: --no-default-features --features hardware
install_libudev: false
- name: browser-native
args: --no-default-features --features browser-native
install_libudev: false
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
with:
key: features-${{ matrix.name }}
- name: Install Linux system dependencies for all-features
if: matrix.install_libudev
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libudev-dev pkg-config
- name: Check feature combination
run: cargo check --locked ${{ matrix.args }}
-55
View File
@@ -1,55 +0,0 @@
name: Main Promotion Gate
on:
pull_request:
branches: [main]
concurrency:
group: main-promotion-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
jobs:
enforce-dev-promotion:
name: Enforce Dev -> Main Promotion
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Validate PR source branch
shell: bash
env:
HEAD_REF: ${{ github.head_ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
BASE_REPO: ${{ github.repository }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
pr_author_lc="$(echo "${PR_AUTHOR}" | tr '[:upper:]' '[:lower:]')"
allowed_authors=("willsarg" "theonlyhennygod")
is_allowed_author=false
for allowed in "${allowed_authors[@]}"; do
if [[ "$pr_author_lc" == "$allowed" ]]; then
is_allowed_author=true
break
fi
done
if [[ "$is_allowed_author" != "true" ]]; then
echo "::error::PRs into main are restricted to: willsarg, theonlyhennygod. PR author: ${PR_AUTHOR}. Open this PR against dev instead."
exit 1
fi
if [[ "$HEAD_REPO" != "$BASE_REPO" ]]; then
echo "::error::PRs into main must originate from ${BASE_REPO}:dev or ${BASE_REPO}:release/*. Current head repo: ${HEAD_REPO}."
exit 1
fi
if [[ "$HEAD_REF" != "dev" && ! "$HEAD_REF" =~ ^release/ ]]; then
echo "::error::PRs into main must use head branch 'dev' or 'release/*'. Current head branch: ${HEAD_REF}."
exit 1
fi
echo "Promotion policy satisfied: author=${PR_AUTHOR}, source=${HEAD_REPO}:${HEAD_REF} -> main"
@@ -1,6 +1,6 @@
# Main Branch Delivery Flows
# Master Branch Delivery Flows
This document explains what runs when code is proposed to `dev`, promoted to `main`, and released.
This document explains what runs when code is proposed to `master` and released.
Use this with:
@@ -8,13 +8,19 @@ Use this with:
- [`docs/pr-workflow.md`](../../docs/pr-workflow.md)
- [`docs/release-process.md`](../../docs/release-process.md)
## Branching Model
ZeroClaw uses a single default branch: `master`. All contributor PRs target `master` directly. There is no `dev` or promotion branch.
Current maintainers with PR approval authority: `theonlyhennygod` and `jordanthejet`.
## 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`, `main-promotion-gate.yml` (for `main` PRs), plus path-scoped workflows |
| Push to `dev`/`main` | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows |
| PR activity (`pull_request`) | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows |
| Push to `master` | `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, `pub-homebrew-core.yml` (manual), `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` |
@@ -27,8 +33,8 @@ Observed averages below are from recent completed runs (sampled from GitHub Acti
| `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 |
| `ci-run.yml` | PR + push to `master` | 74.7s | No | No | No |
| `sec-audit.yml` | PR + push to `master` | 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 |
@@ -45,9 +51,9 @@ Notes:
## Step-By-Step
### 1) PR from branch in this repository -> `dev`
### 1) PR from branch in this repository -> `master`
1. Contributor opens or updates PR against `dev`.
1. Contributor opens or updates PR against `master`.
2. `pull_request_target` automation runs (typical runtime):
- `pr-intake-checks.yml` posts intake warnings/errors.
- `pr-labeler.yml` sets size/risk/scope labels.
@@ -71,15 +77,14 @@ Notes:
- `test`
- `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).
8. `lint-feedback` posts actionable comment if lint/docs gates fail.
9. `CI Required Gate` aggregates results to final pass/fail.
10. Maintainer (`theonlyhennygod` or `jordanthejet`) merges PR once checks and review policy are satisfied.
11. Merge emits a `push` event on `master` (see scenario 3).
### 2) PR from fork -> `dev`
### 2) PR from fork -> `master`
1. External contributor opens PR from `fork/<branch>` into `zeroclaw:dev`.
1. External contributor opens PR from `fork/<branch>` into `zeroclaw:master`.
2. Immediately on `opened`:
- `pull_request_target` workflows start with base-repo context and base-repo token:
- `pr-intake-checks.yml`
@@ -109,23 +114,13 @@ Notes:
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).
9. After merge, normal `push` workflows on `master` execute (scenario 3).
### 3) Promotion PR `dev` -> `main`
### 3) Push to `master` (including after merge)
1. Maintainer opens PR with head `dev` and base `main`.
2. `main-promotion-gate.yml` runs and fails unless PR author is `willsarg` or `theonlyhennygod`.
3. `main-promotion-gate.yml` also fails if head repo/branch is not `<this-repo>:dev`.
4. `ci-run.yml` and `sec-audit.yml` run on the promotion PR.
5. Maintainer merges PR once checks and review policy pass.
6. Merge emits a `push` event on `main`.
### 4) Push to `dev` or `main` (including after merge)
1. Commit reaches `dev` or `main` (usually from a merged PR).
1. Commit reaches `master` (usually from a merged PR).
2. `ci-run.yml` runs on `push`.
3. `sec-audit.yml` runs on `push`.
4. Path-filtered workflows run only if touched files match their filters.
@@ -140,7 +135,7 @@ Workflow: `.github/workflows/pub-docker-img.yml`
### PR behavior
1. Triggered on `pull_request` to `dev` or `main` when Docker build-input paths change.
1. Triggered on `pull_request` to `master` 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`.
@@ -157,7 +152,7 @@ Workflow: `.github/workflows/pub-docker-img.yml`
6. Typical runtime in recent sample: ~139.9s.
7. Result: pushed image tags under `ghcr.io/<owner>/<repo>`.
Important: Docker publish now requires a `v*` tag push; regular `dev`/`main` branch pushes do not publish images.
Important: Docker publish requires a `v*` tag push; regular `master` branch pushes do not publish images.
## Release Logic
@@ -190,11 +185,11 @@ Manual Homebrew formula flow:
## Mermaid Diagrams
### PR to Dev
### PR to Master
```mermaid
flowchart TD
A["PR opened or updated -> dev"] --> B["pull_request_target lane"]
A["PR opened or updated -> master"] --> B["pull_request_target lane"]
B --> B1["pr-intake-checks.yml"]
B --> B2["pr-labeler.yml"]
B --> B3["pr-auto-response.yml"]
@@ -208,19 +203,14 @@ flowchart TD
D --> E{"Checks + review policy pass?"}
E -->|No| F["PR stays open"]
E -->|Yes| G["Merge PR"]
G --> H["push event on dev"]
G --> H["push event on master"]
```
### Promotion and Release
### Release
```mermaid
flowchart TD
D0["Commit reaches dev"] --> B0["ci-run.yml"]
D0 --> C0["sec-audit.yml"]
P["Promotion PR dev -> main"] --> PG["main-promotion-gate.yml"]
PG --> M["Merge to main"]
M --> A["Commit reaches main"]
A --> B["ci-run.yml"]
A["Commit reaches master"] --> B["ci-run.yml"]
A --> C["sec-audit.yml"]
A --> D["path-scoped workflows (if matched)"]
T["Tag push v*"] --> R["pub-release.yml"]
-86
View File
@@ -1,86 +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:
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: ubuntu-latest
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: ubuntu-latest
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: ubuntu-latest
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 });
-44
View File
@@ -1,44 +0,0 @@
name: PR Check Stale
on:
schedule:
- cron: "20 2 * * *"
workflow_dispatch:
permissions: {}
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
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.
-32
View File
@@ -1,32 +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
jobs:
nudge-stale-prs:
runs-on: ubuntu-latest
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 });
-31
View File
@@ -1,31 +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
jobs:
intake:
name: Intake Checks
runs-on: ubuntu-latest
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 });
@@ -1,74 +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
jobs:
contributor-tier-consistency:
runs-on: blacksmith-2vcpu-ubuntu-2404
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
-53
View File
@@ -1,53 +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:
LABEL_POLICY_PATH: .github/label-policy.json
jobs:
label:
runs-on: ubuntu-latest
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 });
+193
View File
@@ -0,0 +1,193 @@
name: Promote Release
on:
workflow_dispatch:
inputs:
version:
description: "Stable version to release (e.g. 0.2.0)"
required: true
type: string
concurrency:
group: promote-release
cancel-in-progress: false
permissions:
contents: write
packages: write
env:
CARGO_TERM_COLOR: always
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
validate:
name: Validate Version
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.check.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Validate semver and Cargo.toml match
id: check
shell: bash
run: |
set -euo pipefail
input_version="${{ inputs.version }}"
cargo_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
if [[ ! "$input_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Version must be semver (X.Y.Z). Got: ${input_version}"
exit 1
fi
if [[ "$cargo_version" != "$input_version" ]]; then
echo "::error::Cargo.toml version (${cargo_version}) does not match input (${input_version}). Bump Cargo.toml first."
exit 1
fi
tag="v${input_version}"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "::error::Tag ${tag} already exists."
exit 1
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.target }}
needs: [validate]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: zeroclaw
ext: tar.gz
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
artifact: zeroclaw
ext: tar.gz
cross_compiler: gcc-aarch64-linux-gnu
linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
linker: aarch64-linux-gnu-gcc
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
artifact: zeroclaw
ext: tar.gz
cross_compiler: gcc-arm-linux-gnueabihf
linker_env: CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER
linker: arm-linux-gnueabihf-gcc
- os: macos-14
target: aarch64-apple-darwin
artifact: zeroclaw
ext: tar.gz
- os: macos-14
target: x86_64-apple-darwin
artifact: zeroclaw
ext: tar.gz
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: zeroclaw.exe
ext: zip
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
if: runner.os != 'Windows'
- name: Install cross compiler
if: matrix.cross_compiler
run: |
sudo apt-get update -qq
sudo apt-get install -y ${{ matrix.cross_compiler }}
- name: Build release
shell: bash
run: |
if [ -n "${{ matrix.linker_env || '' }}" ] && [ -n "${{ matrix.linker || '' }}" ]; then
export "${{ matrix.linker_env }}=${{ matrix.linker }}"
fi
cargo build --release --locked --target ${{ matrix.target }}
- name: Package (Unix)
if: runner.os != 'Windows'
run: |
cd target/${{ matrix.target }}/release
tar czf ../../../zeroclaw-${{ matrix.target }}.${{ matrix.ext }} ${{ matrix.artifact }}
- name: Package (Windows)
if: runner.os == 'Windows'
run: |
cd target/${{ matrix.target }}/release
7z a ../../../zeroclaw-${{ matrix.target }}.${{ matrix.ext }} ${{ matrix.artifact }}
- uses: actions/upload-artifact@v4
with:
name: zeroclaw-${{ matrix.target }}
path: zeroclaw-${{ matrix.target }}.${{ matrix.ext }}
retention-days: 14
publish:
name: Publish Stable Release
needs: [validate, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate checksums
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
cat SHA256SUMS
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ needs.validate.outputs.tag }}" \
--repo "${{ github.repository }}" \
--target "$GITHUB_SHA" \
--title "${{ needs.validate.outputs.tag }}" \
--latest \
--generate-notes \
artifacts/*/*.tar.gz artifacts/*/*.zip artifacts/SHA256SUMS
docker:
name: Push Docker Image
needs: [validate, build]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.tag }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
-175
View File
@@ -1,175 +0,0 @@
name: Pub Docker Img
on:
push:
tags: ["v*"]
pull_request:
branches: [dev, main]
paths:
- "Dockerfile"
- ".dockerignore"
- "docker-compose.yml"
- "rust-toolchain.toml"
- "dev/config.template.toml"
- ".github/workflows/pub-docker-img.yml"
workflow_dispatch:
concurrency:
group: docker-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
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)
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 25
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@ef12d5b165b596e3aa44ea8198d8fde563eab402 # v1
- name: Extract metadata (tags, labels)
if: github.event_name == 'pull_request'
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=pr
- name: Build smoke image
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
push: false
load: true
provenance: false
sbom: false
tags: zeroclaw-pr-smoke:latest
labels: ${{ steps.meta.outputs.labels || '' }}
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Verify image
run: docker run --rm zeroclaw-pr-smoke:latest --version
publish:
name: Build and Push Docker Image
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository == 'zeroclaw-labs/zeroclaw'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 45
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@ef12d5b165b596e3aa44ea8198d8fde563eab402 # v1
- name: Log in to Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute tags
id: meta
shell: bash
run: |
set -euo pipefail
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
SHA_TAG="${IMAGE}:sha-${GITHUB_SHA::12}"
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Docker publish is restricted to v* tag pushes."
exit 1
fi
TAG_NAME="${GITHUB_REF#refs/tags/}"
TAGS="${IMAGE}:${TAG_NAME},${SHA_TAG}"
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
- name: Build and push Docker image
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Set GHCR package visibility to public
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
owner="${GITHUB_REPOSITORY_OWNER,,}"
repo="${GITHUB_REPOSITORY#*/}"
# Package path can vary depending on repository/package linkage.
candidates=(
"$repo"
"${owner}%2F${repo}"
)
for scope in orgs users; do
for pkg in "${candidates[@]}"; do
code="$(curl -sS -o /tmp/ghcr-visibility.json -w "%{http_code}" \
-X PATCH \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/${scope}/${owner}/packages/container/${pkg}/visibility" \
-d '{"visibility":"public"}' || true)"
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "GHCR package visibility is public (${scope}/${owner}/${pkg})."
exit 0
fi
echo "Visibility attempt ${scope}/${owner}/${pkg} returned HTTP ${code}."
done
done
echo "::warning::Unable to update GHCR visibility via API in this run; proceeding to direct anonymous pull verification."
- name: Verify anonymous GHCR pull access
shell: bash
run: |
set -euo pipefail
TAG_NAME="${GITHUB_REF#refs/tags/}"
token_resp="$(curl -sS "https://ghcr.io/token?scope=repository:${GITHUB_REPOSITORY}:pull")"
token="$(echo "$token_resp" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
if [ -z "$token" ]; then
echo "::error::Anonymous GHCR token request failed: $token_resp"
exit 1
fi
code="$(curl -sS -o /tmp/ghcr-manifest.json -w "%{http_code}" \
-H "Authorization: Bearer ${token}" \
-H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
"https://ghcr.io/v2/${GITHUB_REPOSITORY}/manifests/${TAG_NAME}")"
if [ "$code" != "200" ]; then
echo "::error::Anonymous manifest pull failed with HTTP ${code}"
cat /tmp/ghcr-manifest.json || true
exit 1
fi
echo "Anonymous GHCR pull access verified."
-221
View File
@@ -1,221 +0,0 @@
name: Pub Homebrew Core
on:
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to publish (vX.Y.Z)"
required: true
type: string
dry_run:
description: "Patch formula only (no push/PR)"
required: false
default: true
type: boolean
concurrency:
group: homebrew-core-${{ github.run_id }}
cancel-in-progress: false
permissions:
contents: read
jobs:
publish-homebrew-core:
name: Publish Homebrew Core PR
runs-on: blacksmith-2vcpu-ubuntu-2404
env:
UPSTREAM_REPO: Homebrew/homebrew-core
FORMULA_PATH: Formula/z/zeroclaw.rb
RELEASE_TAG: ${{ inputs.release_tag }}
DRY_RUN: ${{ inputs.dry_run }}
BOT_FORK_REPO: ${{ vars.HOMEBREW_CORE_BOT_FORK_REPO }}
BOT_EMAIL: ${{ vars.HOMEBREW_CORE_BOT_EMAIL }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Validate release tag and version alignment
id: release_meta
shell: bash
run: |
set -euo pipefail
semver_pattern='^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'
if [[ ! "$RELEASE_TAG" =~ $semver_pattern ]]; then
echo "::error::release_tag must match semver-like format (vX.Y.Z[-suffix])."
exit 1
fi
if ! git rev-parse "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
git fetch --tags origin
fi
tag_version="${RELEASE_TAG#v}"
cargo_version="$(git show "${RELEASE_TAG}:Cargo.toml" | sed -n 's/^version = "\([^"]*\)"/\1/p' | head -n1)"
if [[ -z "$cargo_version" ]]; then
echo "::error::Unable to read Cargo.toml version from tag ${RELEASE_TAG}."
exit 1
fi
if [[ "$cargo_version" != "$tag_version" ]]; then
echo "::error::Tag ${RELEASE_TAG} does not match Cargo.toml version (${cargo_version})."
echo "::error::Bump Cargo.toml first, then publish Homebrew."
exit 1
fi
tarball_url="https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${RELEASE_TAG}.tar.gz"
tarball_sha="$(curl -fsSL "$tarball_url" | sha256sum | awk '{print $1}')"
{
echo "tag_version=$tag_version"
echo "tarball_url=$tarball_url"
echo "tarball_sha=$tarball_sha"
} >> "$GITHUB_OUTPUT"
{
echo "### Release Metadata"
echo "- release_tag: ${RELEASE_TAG}"
echo "- cargo_version: ${cargo_version}"
echo "- tarball_sha256: ${tarball_sha}"
echo "- dry_run: ${DRY_RUN}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Patch Homebrew formula
id: patch_formula
shell: bash
env:
HOMEBREW_CORE_BOT_TOKEN: ${{ secrets.HOMEBREW_UPSTREAM_PR_TOKEN || secrets.HOMEBREW_CORE_BOT_TOKEN }}
GH_TOKEN: ${{ secrets.HOMEBREW_UPSTREAM_PR_TOKEN || secrets.HOMEBREW_CORE_BOT_TOKEN }}
run: |
set -euo pipefail
tmp_repo="$(mktemp -d)"
echo "tmp_repo=$tmp_repo" >> "$GITHUB_OUTPUT"
if [[ "$DRY_RUN" == "true" ]]; then
git clone --depth=1 "https://github.com/${UPSTREAM_REPO}.git" "$tmp_repo/homebrew-core"
else
if [[ -z "${BOT_FORK_REPO}" ]]; then
echo "::error::Repository variable HOMEBREW_CORE_BOT_FORK_REPO is required when dry_run=false."
exit 1
fi
if [[ -z "${HOMEBREW_CORE_BOT_TOKEN}" ]]; then
echo "::error::Repository secret HOMEBREW_CORE_BOT_TOKEN is required when dry_run=false."
exit 1
fi
if [[ "$BOT_FORK_REPO" != */* ]]; then
echo "::error::HOMEBREW_CORE_BOT_FORK_REPO must be in owner/repo format."
exit 1
fi
if ! command -v gh >/dev/null 2>&1; then
echo "::error::gh CLI is required on the runner."
exit 1
fi
if [[ -z "${GH_TOKEN:-}" ]]; then
echo "::error::Repository secret HOMEBREW_CORE_BOT_TOKEN is missing."
exit 1
fi
if ! gh api "repos/${BOT_FORK_REPO}" >/dev/null 2>&1; then
echo "::error::HOMEBREW_CORE_BOT_TOKEN cannot access ${BOT_FORK_REPO}."
exit 1
fi
gh repo clone "${BOT_FORK_REPO}" "$tmp_repo/homebrew-core" -- --depth=1
fi
repo_dir="$tmp_repo/homebrew-core"
formula_file="$repo_dir/$FORMULA_PATH"
if [[ ! -f "$formula_file" ]]; then
echo "::error::Formula file not found: $FORMULA_PATH"
exit 1
fi
if [[ "$DRY_RUN" == "false" ]]; then
if git -C "$repo_dir" remote get-url upstream >/dev/null 2>&1; then
git -C "$repo_dir" remote set-url upstream "https://github.com/${UPSTREAM_REPO}.git"
else
git -C "$repo_dir" remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
fi
if git -C "$repo_dir" ls-remote --exit-code --heads upstream main >/dev/null 2>&1; then
upstream_ref="main"
else
upstream_ref="master"
fi
git -C "$repo_dir" fetch --depth=1 upstream "$upstream_ref"
branch_name="zeroclaw-${RELEASE_TAG}-${GITHUB_RUN_ID}"
git -C "$repo_dir" checkout -B "$branch_name" "upstream/$upstream_ref"
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
fi
tarball_url="${{ steps.release_meta.outputs.tarball_url }}"
tarball_sha="${{ steps.release_meta.outputs.tarball_sha }}"
perl -0pi -e "s|^ url \".*\"| url \"${tarball_url}\"|m" "$formula_file"
perl -0pi -e "s|^ sha256 \".*\"| sha256 \"${tarball_sha}\"|m" "$formula_file"
perl -0pi -e "s|^ license \".*\"| license \"Apache-2.0 OR MIT\"|m" "$formula_file"
perl -0pi -e 's|^ head "https://github\.com/zeroclaw-labs/zeroclaw\.git".*| head "https://github.com/zeroclaw-labs/zeroclaw.git"|m' "$formula_file"
git -C "$repo_dir" diff -- "$FORMULA_PATH" > "$tmp_repo/formula.diff"
if [[ ! -s "$tmp_repo/formula.diff" ]]; then
echo "::error::No formula changes generated. Nothing to publish."
exit 1
fi
{
echo "### Formula Diff"
echo '```diff'
cat "$tmp_repo/formula.diff"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Push branch and open Homebrew PR
if: ${{ inputs.dry_run == false }}
shell: bash
env:
GH_TOKEN: ${{ secrets.HOMEBREW_UPSTREAM_PR_TOKEN || secrets.HOMEBREW_CORE_BOT_TOKEN }}
run: |
set -euo pipefail
repo_dir="${{ steps.patch_formula.outputs.tmp_repo }}/homebrew-core"
branch_name="${{ steps.patch_formula.outputs.branch_name }}"
tag_version="${{ steps.release_meta.outputs.tag_version }}"
fork_owner="${BOT_FORK_REPO%%/*}"
bot_email="${BOT_EMAIL:-${fork_owner}@users.noreply.github.com}"
git -C "$repo_dir" config user.name "$fork_owner"
git -C "$repo_dir" config user.email "$bot_email"
git -C "$repo_dir" add "$FORMULA_PATH"
git -C "$repo_dir" commit -m "zeroclaw ${tag_version}"
if [[ -z "${GH_TOKEN:-}" ]]; then
echo "::error::Repository secret HOMEBREW_CORE_BOT_TOKEN is missing."
exit 1
fi
gh auth setup-git
git -C "$repo_dir" push --set-upstream origin "$branch_name"
pr_title="zeroclaw ${tag_version}"
pr_body=$(cat <<EOF
Automated formula bump from ZeroClaw release workflow.
- Release tag: ${RELEASE_TAG}
- Source tarball: ${{ steps.release_meta.outputs.tarball_url }}
- Source sha256: ${{ steps.release_meta.outputs.tarball_sha }}
EOF
)
gh pr create \
--repo "$UPSTREAM_REPO" \
--base main \
--head "${fork_owner}:${branch_name}" \
--title "$pr_title" \
--body "$pr_body"
- name: Summary output
shell: bash
run: |
set -euo pipefail
if [[ "$DRY_RUN" == "true" ]]; then
echo "Dry run complete: formula diff generated, no push/PR performed."
else
echo "Publish complete: branch pushed and PR opened from bot fork."
fi
-435
View File
@@ -1,435 +0,0 @@
name: Pub Release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
release_ref:
description: "Git ref (branch, tag, or SHA) to build"
required: false
default: "main"
type: string
publish_release:
description: "Publish a GitHub release (false = verification build only)"
required: false
default: false
type: boolean
release_tag:
description: "Existing release tag (required when publish_release=true), e.g. v0.1.1"
required: false
default: ""
type: string
draft:
description: "Create release as draft (manual publish only)"
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 }}
cancel-in-progress: false
permissions:
contents: write
packages: read
id-token: write # Required for cosign keyless signing via OIDC
env:
CARGO_TERM_COLOR: always
jobs:
prepare:
name: Prepare Release Context
runs-on: blacksmith-2vcpu-ubuntu-2404
outputs:
release_ref: ${{ steps.vars.outputs.release_ref }}
release_tag: ${{ steps.vars.outputs.release_tag }}
publish_release: ${{ steps.vars.outputs.publish_release }}
draft_release: ${{ steps.vars.outputs.draft_release }}
steps:
- name: Resolve release inputs
id: vars
shell: bash
run: |
set -euo pipefail
event_name="${GITHUB_EVENT_NAME}"
publish_release="false"
draft_release="false"
semver_pattern='^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'
if [[ "$event_name" == "push" ]]; then
release_ref="${GITHUB_REF_NAME}"
release_tag="${GITHUB_REF_NAME}"
publish_release="true"
elif [[ "$event_name" == "workflow_dispatch" ]]; then
release_ref="${{ inputs.release_ref }}"
publish_release="${{ inputs.publish_release }}"
draft_release="${{ inputs.draft }}"
if [[ "$publish_release" == "true" ]]; then
release_tag="${{ inputs.release_tag }}"
if [[ -z "$release_tag" ]]; then
echo "::error::release_tag is required when publish_release=true"
exit 1
fi
release_ref="$release_tag"
else
release_tag="verify-${GITHUB_SHA::12}"
fi
else
# schedule
release_ref="main"
release_tag="verify-${GITHUB_SHA::12}"
fi
if [[ "$publish_release" == "true" ]]; then
if [[ ! "$release_tag" =~ $semver_pattern ]]; then
echo "::error::release_tag must match semver-like format (vX.Y.Z[-suffix])"
exit 1
fi
if ! git ls-remote --exit-code --tags "https://github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${release_tag}" >/dev/null; then
echo "::error::Tag ${release_tag} does not exist on origin. Push the tag first, then rerun manual publish."
exit 1
fi
# Guardrail: release tags must resolve to commits already reachable from main.
tmp_repo="$(mktemp -d)"
trap 'rm -rf "$tmp_repo"' EXIT
git -C "$tmp_repo" init -q
git -C "$tmp_repo" remote add origin "https://github.com/${GITHUB_REPOSITORY}.git"
git -C "$tmp_repo" fetch --quiet --filter=blob:none origin main "refs/tags/${release_tag}:refs/tags/${release_tag}"
if ! git -C "$tmp_repo" merge-base --is-ancestor "refs/tags/${release_tag}" "origin/main"; then
echo "::error::Tag ${release_tag} is not reachable from origin/main. Release tags must be cut from main."
exit 1
fi
# Guardrail: release tag and Cargo package version must stay aligned.
tag_version="${release_tag#v}"
cargo_version="$(git -C "$tmp_repo" show "refs/tags/${release_tag}:Cargo.toml" | sed -n 's/^version = "\([^"]*\)"/\1/p' | head -n1)"
if [[ -z "$cargo_version" ]]; then
echo "::error::Unable to read Cargo package version from ${release_tag}:Cargo.toml"
exit 1
fi
if [[ "$cargo_version" != "$tag_version" ]]; then
echo "::error::Tag ${release_tag} does not match Cargo.toml version (${cargo_version})."
echo "::error::Bump Cargo.toml version first, then create/publish the matching tag."
exit 1
fi
fi
{
echo "release_ref=${release_ref}"
echo "release_tag=${release_tag}"
echo "publish_release=${publish_release}"
echo "draft_release=${draft_release}"
} >> "$GITHUB_OUTPUT"
{
echo "### Release Context"
echo "- event: ${event_name}"
echo "- release_ref: ${release_ref}"
echo "- release_tag: ${release_tag}"
echo "- publish_release: ${publish_release}"
echo "- draft_release: ${draft_release}"
} >> "$GITHUB_STEP_SUMMARY"
build-release:
name: Build ${{ matrix.target }}
needs: [prepare]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: gcc-aarch64-linux-gnu
linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
linker: aarch64-linux-gnu-gcc
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: gcc-arm-linux-gnueabihf
linker_env: CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER
linker: arm-linux-gnueabihf-gcc
- os: ubuntu-latest
target: armv7-linux-androideabi
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
android_ndk: true
android_api: 21
- os: ubuntu-latest
target: aarch64-linux-android
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
android_ndk: true
android_api: 21
- os: macos-15-intel
target: x86_64-apple-darwin
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
- os: macos-14
target: aarch64-apple-darwin
artifact: zeroclaw
archive_ext: tar.gz
cross_compiler: ""
linker_env: ""
linker: ""
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: zeroclaw.exe
archive_ext: zip
cross_compiler: ""
linker_env: ""
linker: ""
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: 1.92.0
targets: ${{ matrix.target }}
- uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
if: runner.os != 'Windows'
- 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 }}
- name: Setup Android NDK
if: matrix.android_ndk
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26d
add-to-path: true
- name: Configure Android toolchain
if: matrix.android_ndk
run: |
echo "Setting up Android NDK toolchain for ${{ matrix.target }}"
NDK_HOME="${{ steps.setup-ndk.outputs.ndk-path }}"
TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
# Add to path for linker resolution
echo "$TOOLCHAIN" >> "$GITHUB_PATH"
# Set linker environment variables
if [[ "${{ matrix.target }}" == "armv7-linux-androideabi" ]]; then
echo "CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=${TOOLCHAIN}/armv7a-linux-androideabi${{ matrix.android_api }}-clang" >> "$GITHUB_ENV"
elif [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then
echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${TOOLCHAIN}/aarch64-linux-android${{ matrix.android_api }}-clang" >> "$GITHUB_ENV"
fi
- name: Build release
shell: bash
env:
LINKER_ENV: ${{ matrix.linker_env }}
LINKER: ${{ matrix.linker }}
run: |
if [ -n "$LINKER_ENV" ] && [ -n "$LINKER" ]; then
echo "Using linker override: $LINKER_ENV=$LINKER"
export "$LINKER_ENV=$LINKER"
fi
cargo build --profile release-fast --locked --target ${{ matrix.target }}
- name: Check binary size (Unix)
if: runner.os != 'Windows'
run: bash scripts/ci/check_binary_size.sh "target/${{ matrix.target }}/release-fast/${{ matrix.artifact }}" "${{ matrix.target }}"
- name: Package (Unix)
if: runner.os != 'Windows'
run: |
cd target/${{ matrix.target }}/release-fast
tar czf ../../../zeroclaw-${{ matrix.target }}.${{ matrix.archive_ext }} ${{ matrix.artifact }}
- name: Package (Windows)
if: runner.os == 'Windows'
run: |
cd target/${{ matrix.target }}/release-fast
7z a ../../../zeroclaw-${{ matrix.target }}.${{ matrix.archive_ext }} ${{ matrix.artifact }}
- name: Upload artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: zeroclaw-${{ matrix.target }}
path: zeroclaw-${{ matrix.target }}.${{ matrix.archive_ext }}
retention-days: 7
verify-artifacts:
name: Verify Artifact Set
needs: [prepare, build-release]
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Download all artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: artifacts
- name: Validate expected archives
shell: bash
run: |
set -euo pipefail
expected=(
"zeroclaw-x86_64-unknown-linux-gnu.tar.gz"
"zeroclaw-aarch64-unknown-linux-gnu.tar.gz"
"zeroclaw-armv7-unknown-linux-gnueabihf.tar.gz"
"zeroclaw-armv7-linux-androideabi.tar.gz"
"zeroclaw-aarch64-linux-android.tar.gz"
"zeroclaw-x86_64-apple-darwin.tar.gz"
"zeroclaw-aarch64-apple-darwin.tar.gz"
"zeroclaw-x86_64-pc-windows-msvc.zip"
)
missing=0
for file in "${expected[@]}"; do
if ! find artifacts -type f -name "$file" -print -quit | grep -q .; then
echo "::error::Missing release archive: $file"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi
echo "All expected release archives are present."
publish:
name: Publish Release
if: needs.prepare.outputs.publish_release == 'true'
needs: [prepare, verify-artifacts]
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 45
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- name: Download all artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: artifacts
- name: Install syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Generate SBOM (CycloneDX)
run: |
syft dir:. --source-name zeroclaw -o cyclonedx-json=artifacts/zeroclaw.cdx.json -o spdx-json=artifacts/zeroclaw.spdx.json
{
echo "### SBOM Generated"
echo "- CycloneDX: zeroclaw.cdx.json"
echo "- SPDX: zeroclaw.spdx.json"
} >> "$GITHUB_STEP_SUMMARY"
- name: Attach license and notice files
run: |
cp LICENSE-APACHE artifacts/LICENSE-APACHE
cp LICENSE-MIT artifacts/LICENSE-MIT
cp NOTICE artifacts/NOTICE
- name: Generate SHA256 checksums
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.cdx.json' -o -name '*.spdx.json' -o -name 'LICENSE-APACHE' -o -name 'LICENSE-MIT' -o -name 'NOTICE' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
echo "Generated checksums:"
cat SHA256SUMS
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Sign artifacts with cosign (keyless)
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
cosign sign-blob --yes \
--bundle="${file}.sigstore.json" \
--output-signature="${file}.sig" \
--output-certificate="${file}.pem" \
"$file"
done < <(find artifacts -type f ! -name '*.sig' ! -name '*.pem' ! -name '*.sigstore.json' -print0)
- name: Verify GHCR release tag availability
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
run: |
set -euo pipefail
repo="${GITHUB_REPOSITORY,,}"
manifest_url="https://ghcr.io/v2/${repo}/manifests/${RELEASE_TAG}"
accept_header="application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json"
max_attempts=75
sleep_seconds=20
for attempt in $(seq 1 "$max_attempts"); do
token_resp="$(curl -sS "https://ghcr.io/token?scope=repository:${repo}:pull" || true)"
token="$(echo "$token_resp" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
if [ -z "$token" ]; then
code="000"
else
code="$(curl -sS -o /tmp/ghcr-release-manifest.json -w "%{http_code}" \
-H "Authorization: Bearer ${token}" \
-H "Accept: ${accept_header}" \
"${manifest_url}" || true)"
fi
if [ "$code" = "200" ]; then
echo "GHCR release tag is available: ${repo}:${RELEASE_TAG}"
exit 0
fi
if [ "$attempt" -lt "$max_attempts" ]; then
echo "Waiting for GHCR tag ${repo}:${RELEASE_TAG} (attempt ${attempt}/${max_attempts}, HTTP ${code})..."
sleep "$sleep_seconds"
fi
done
echo "::error::GHCR tag ${repo}:${RELEASE_TAG} was not available before release publish timeout."
cat /tmp/ghcr-release-manifest.json || true
exit 1
- name: Create GitHub Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
tag_name: ${{ needs.prepare.outputs.release_tag }}
draft: ${{ needs.prepare.outputs.draft_release == 'true' }}
generate_release_notes: true
files: |
artifacts/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+177
View File
@@ -0,0 +1,177 @@
name: Daily Beta Release
on:
schedule:
- cron: '0 8 * * *' # Daily at 08:00 UTC
workflow_dispatch: {}
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: write
packages: write
env:
CARGO_TERM_COLOR: always
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
version:
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
name: Resolve Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Compute beta version
id: ver
shell: bash
run: |
set -euo pipefail
base_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)
beta_tag="v${base_version}-beta.${GITHUB_RUN_NUMBER}"
echo "version=${base_version}" >> "$GITHUB_OUTPUT"
echo "tag=${beta_tag}" >> "$GITHUB_OUTPUT"
echo "Beta release: ${beta_tag}"
build:
name: Build ${{ matrix.target }}
needs: [version]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: zeroclaw
ext: tar.gz
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
artifact: zeroclaw
ext: tar.gz
cross_compiler: gcc-aarch64-linux-gnu
linker_env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
linker: aarch64-linux-gnu-gcc
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
artifact: zeroclaw
ext: tar.gz
cross_compiler: gcc-arm-linux-gnueabihf
linker_env: CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER
linker: arm-linux-gnueabihf-gcc
- os: macos-14
target: aarch64-apple-darwin
artifact: zeroclaw
ext: tar.gz
- os: macos-14
target: x86_64-apple-darwin
artifact: zeroclaw
ext: tar.gz
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: zeroclaw.exe
ext: zip
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
if: runner.os != 'Windows'
- name: Install cross compiler
if: matrix.cross_compiler
run: |
sudo apt-get update -qq
sudo apt-get install -y ${{ matrix.cross_compiler }}
- name: Build release
shell: bash
run: |
if [ -n "${{ matrix.linker_env || '' }}" ] && [ -n "${{ matrix.linker || '' }}" ]; then
export "${{ matrix.linker_env }}=${{ matrix.linker }}"
fi
cargo build --release --locked --target ${{ matrix.target }}
- name: Package (Unix)
if: runner.os != 'Windows'
run: |
cd target/${{ matrix.target }}/release
tar czf ../../../zeroclaw-${{ matrix.target }}.${{ matrix.ext }} ${{ matrix.artifact }}
- name: Package (Windows)
if: runner.os == 'Windows'
run: |
cd target/${{ matrix.target }}/release
7z a ../../../zeroclaw-${{ matrix.target }}.${{ matrix.ext }} ${{ matrix.artifact }}
- uses: actions/upload-artifact@v4
with:
name: zeroclaw-${{ matrix.target }}
path: zeroclaw-${{ matrix.target }}.${{ matrix.ext }}
retention-days: 7
publish:
name: Publish Beta Release
needs: [version, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate checksums
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec sha256sum {} + | sed 's| \./[^/]*/| |' > SHA256SUMS
cat SHA256SUMS
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ needs.version.outputs.tag }}" \
--repo "${{ github.repository }}" \
--target "$GITHUB_SHA" \
--title "${{ needs.version.outputs.tag }}" \
--prerelease \
--generate-notes \
artifacts/*/*.tar.gz artifacts/*/*.zip artifacts/SHA256SUMS
docker:
name: Push Docker Image
needs: [version, build]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.tag }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:beta
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -1,54 +0,0 @@
// Enforce ownership rules for root license files in PRs.
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request?.number;
const prAuthor = context.payload.pull_request?.user?.login?.toLowerCase() || "";
if (!prNumber) {
core.setFailed("Missing pull_request context.");
return;
}
const ownerAllowlist = ["willsarg"];
if (ownerAllowlist.length === 0) {
core.setFailed("License owner allowlist is empty.");
return;
}
const protectedFiles = new Set(["LICENSE-APACHE", "LICENSE-MIT"]);
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const changedProtectedFiles = files
.map((file) => file.filename)
.filter((name) => protectedFiles.has(name));
if (changedProtectedFiles.length === 0) {
core.info("No protected root license files changed in this PR.");
return;
}
core.info(`Protected license files changed:\n- ${changedProtectedFiles.join("\n- ")}`);
core.info(`Allowed license file editors: ${ownerAllowlist.join(", ")}`);
if (!prAuthor) {
core.setFailed("Unable to resolve PR author login.");
return;
}
if (!ownerAllowlist.includes(prAuthor)) {
core.setFailed(
`Root license files (${changedProtectedFiles.join(", ")}) can only be changed by ${ownerAllowlist.join(", ")}. PR author is @${prAuthor}.`,
);
return;
}
core.info(`License file edit authorized for PR author: @${prAuthor}`);
};
@@ -1,83 +0,0 @@
// Extracted from ci-run.yml step: Require owner approval for workflow file changes
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request?.number;
const prAuthor = context.payload.pull_request?.user?.login?.toLowerCase() || "";
if (!prNumber) {
core.setFailed("Missing pull_request context.");
return;
}
const baseOwners = ["theonlyhennygod", "willsarg", "chumyin"];
const configuredOwners = (process.env.WORKFLOW_OWNER_LOGINS || "")
.split(",")
.map((login) => login.trim().toLowerCase())
.filter(Boolean);
const ownerAllowlist = [...new Set([...baseOwners, ...configuredOwners])];
if (ownerAllowlist.length === 0) {
core.setFailed("Workflow owner allowlist is empty.");
return;
}
core.info(`Workflow owner allowlist: ${ownerAllowlist.join(", ")}`);
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const workflowFiles = files
.map((file) => file.filename)
.filter((name) => name.startsWith(".github/workflows/"));
if (workflowFiles.length === 0) {
core.info("No workflow files changed in this PR.");
return;
}
core.info(`Workflow files changed:\n- ${workflowFiles.join("\n- ")}`);
if (prAuthor && ownerAllowlist.includes(prAuthor)) {
core.info(`Workflow PR authored by allowlisted owner: @${prAuthor}`);
return;
}
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const latestReviewByUser = new Map();
for (const review of reviews) {
const login = review.user?.login;
if (!login) continue;
latestReviewByUser.set(login.toLowerCase(), review.state);
}
const approvedUsers = [...latestReviewByUser.entries()]
.filter(([, state]) => state === "APPROVED")
.map(([login]) => login);
if (approvedUsers.length === 0) {
core.setFailed("Workflow files changed but no approving review is present.");
return;
}
const ownerApprover = approvedUsers.find((login) => ownerAllowlist.includes(login));
if (!ownerApprover) {
core.setFailed(
`Workflow files changed. Approvals found (${approvedUsers.join(", ")}), but none match workflow owner allowlist.`,
);
return;
}
core.info(`Workflow owner approval present: @${ownerApprover}`);
};
@@ -1,90 +0,0 @@
// Post actionable lint failure summary as a PR comment.
// Used by the lint-feedback CI job via actions/github-script.
//
// Required environment variables:
// RUST_CHANGED — "true" if Rust files changed
// DOCS_CHANGED — "true" if docs files changed
// LINT_RESULT — result of the lint job
// LINT_DELTA_RESULT — result of the strict delta lint job
// DOCS_RESULT — result of the docs-quality job
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const issueNumber = context.payload.pull_request?.number;
if (!issueNumber) return;
const marker = "<!-- ci-lint-feedback -->";
const rustChanged = process.env.RUST_CHANGED === "true";
const docsChanged = process.env.DOCS_CHANGED === "true";
const lintResult = process.env.LINT_RESULT || "skipped";
const lintDeltaResult = process.env.LINT_DELTA_RESULT || "skipped";
const docsResult = process.env.DOCS_RESULT || "skipped";
const failures = [];
if (rustChanged && !["success", "skipped"].includes(lintResult)) {
failures.push("`Lint Gate (Format + Clippy)` failed.");
}
if (rustChanged && !["success", "skipped"].includes(lintDeltaResult)) {
failures.push("`Lint Gate (Strict Delta)` failed.");
}
if (docsChanged && !["success", "skipped"].includes(docsResult)) {
failures.push("`Docs Quality` failed.");
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
const existing = comments.find((comment) => (comment.body || "").includes(marker));
if (failures.length === 0) {
if (existing) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existing.id,
});
}
core.info("No lint/docs gate failures. No feedback comment required.");
return;
}
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const body = [
marker,
"### CI lint feedback",
"",
"This PR failed one or more fast lint/documentation gates:",
"",
...failures.map((item) => `- ${item}`),
"",
"Open the failing logs in this run:",
`- ${runUrl}`,
"",
"Local fix commands:",
"- `./scripts/ci/rust_quality_gate.sh`",
"- `./scripts/ci/rust_strict_delta_gate.sh`",
"- `./scripts/ci/docs_quality_gate.sh`",
"",
"After fixes, push a new commit and CI will re-run automatically.",
].join("\n");
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
}
};
@@ -1,132 +0,0 @@
// Extracted from pr-auto-response.yml step: Apply contributor tier label for issue author
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue = context.payload.issue;
const pullRequest = context.payload.pull_request;
const target = issue ?? pullRequest;
async function loadContributorTierPolicy() {
const policyPath = process.env.LABEL_POLICY_PATH || ".github/label-policy.json";
const fallback = {
contributorTierColor: "2ED9FF",
contributorTierRules: [
{ label: "distinguished contributor", minMergedPRs: 50 },
{ label: "principal contributor", minMergedPRs: 20 },
{ label: "experienced contributor", minMergedPRs: 10 },
{ label: "trusted contributor", minMergedPRs: 5 },
],
};
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path: policyPath,
ref: context.payload.repository?.default_branch || "main",
});
const json = JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
const contributorTierRules = (json.contributor_tiers || []).map((entry) => ({
label: String(entry.label || "").trim(),
minMergedPRs: Number(entry.min_merged_prs || 0),
}));
const contributorTierColor = String(json.contributor_tier_color || "").toUpperCase();
if (!contributorTierColor || contributorTierRules.length === 0) {
return fallback;
}
return { contributorTierColor, contributorTierRules };
} catch (error) {
core.warning(`failed to load ${policyPath}, using fallback policy: ${error.message}`);
return fallback;
}
}
const { contributorTierColor, contributorTierRules } = await loadContributorTierPolicy();
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
const managedContributorLabels = new Set(contributorTierLabels);
const action = context.payload.action;
const changedLabel = context.payload.label?.name;
if (!target) return;
if ((action === "labeled" || action === "unlabeled") && !managedContributorLabels.has(changedLabel)) {
return;
}
const author = target.user;
if (!author || author.type === "Bot") return;
function contributorTierDescription(rule) {
return `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
}
async function ensureContributorTierLabels() {
for (const rule of contributorTierRules) {
const label = rule.label;
const expectedDescription = contributorTierDescription(rule);
try {
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name: label });
const currentColor = (existing.color || "").toUpperCase();
const currentDescription = (existing.description || "").trim();
if (currentColor !== contributorTierColor || currentDescription !== expectedDescription) {
await github.rest.issues.updateLabel({
owner,
repo,
name: label,
new_name: label,
color: contributorTierColor,
description: expectedDescription,
});
}
} catch (error) {
if (error.status !== 404) throw error;
await github.rest.issues.createLabel({
owner,
repo,
name: label,
color: contributorTierColor,
description: expectedDescription,
});
}
}
}
function selectContributorTier(mergedCount) {
const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
return matchedTier ? matchedTier.label : null;
}
let contributorTierLabel = null;
try {
const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr is:merged author:${author.login}`,
per_page: 1,
});
const mergedCount = mergedSearch.total_count || 0;
contributorTierLabel = selectContributorTier(mergedCount);
} catch (error) {
core.warning(`failed to evaluate contributor tier status: ${error.message}`);
return;
}
await ensureContributorTierLabels();
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: target.number,
});
const keepLabels = currentLabels
.map((label) => label.name)
.filter((label) => !contributorTierLabels.includes(label));
if (contributorTierLabel) {
keepLabels.push(contributorTierLabel);
}
await github.rest.issues.setLabels({
owner,
repo,
issue_number: target.number,
labels: [...new Set(keepLabels)],
});
};
@@ -1,94 +0,0 @@
// Extracted from pr-auto-response.yml step: Handle label-driven responses
module.exports = async ({ github, context, core }) => {
const label = context.payload.label?.name;
if (!label) return;
const issue = context.payload.issue;
const pullRequest = context.payload.pull_request;
const target = issue ?? pullRequest;
if (!target) return;
const isIssue = Boolean(issue);
const issueNumber = target.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const rules = [
{
label: "r:support",
close: true,
closeIssuesOnly: true,
closeReason: "not_planned",
message:
"This looks like a usage/support request. Please use README + docs first, then open a focused bug with repro details if behavior is incorrect.",
},
{
label: "r:needs-repro",
close: false,
message:
"Thanks for the report. Please add deterministic repro steps, exact environment, and redacted logs so maintainers can triage quickly.",
},
{
label: "invalid",
close: true,
closeIssuesOnly: true,
closeReason: "not_planned",
message:
"Closing as invalid based on current information. If this is still relevant, open a new issue with updated evidence and reproducible steps.",
},
{
label: "duplicate",
close: true,
closeIssuesOnly: true,
closeReason: "not_planned",
message:
"Closing as duplicate. Please continue discussion in the canonical linked issue/PR.",
},
];
const rule = rules.find((entry) => entry.label === label);
if (!rule) return;
const marker = `<!-- auto-response:${rule.label} -->`;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
const alreadyCommented = comments.some((comment) =>
(comment.body || "").includes(marker)
);
if (!alreadyCommented) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${rule.message}\n\n${marker}`,
});
}
if (!rule.close) return;
if (rule.closeIssuesOnly && !isIssue) return;
if (target.state === "closed") return;
if (isIssue) {
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: "closed",
state_reason: rule.closeReason || "not_planned",
});
} else {
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: "closed",
});
}
};
@@ -1,161 +0,0 @@
// Extracted from pr-check-status.yml step: Nudge PRs that need rebase or CI refresh
module.exports = async ({ github, context, core }) => {
const staleHours = Number(process.env.STALE_HOURS || "48");
const ignoreLabels = new Set(["no-stale", "stale", "maintainer", "no-pr-hygiene"]);
const marker = "<!-- pr-hygiene-nudge -->";
const owner = context.repo.owner;
const repo = context.repo.repo;
const openPrs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
const activePrs = openPrs.filter((pr) => {
if (pr.draft) {
return false;
}
const labels = new Set((pr.labels || []).map((label) => label.name));
return ![...ignoreLabels].some((label) => labels.has(label));
});
core.info(`Scanning ${activePrs.length} open PR(s) for hygiene nudges.`);
let nudged = 0;
let skipped = 0;
for (const pr of activePrs) {
const { data: headCommit } = await github.rest.repos.getCommit({
owner,
repo,
ref: pr.head.sha,
});
const headCommitAt =
headCommit.commit?.committer?.date || headCommit.commit?.author?.date;
if (!headCommitAt) {
skipped += 1;
core.info(`#${pr.number}: missing head commit timestamp, skipping.`);
continue;
}
const ageHours = (Date.now() - new Date(headCommitAt).getTime()) / 3600000;
if (ageHours < staleHours) {
skipped += 1;
continue;
}
const { data: prDetail } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
});
const isBehindBase = prDetail.mergeable_state === "behind";
const { data: checkRunsData } = await github.rest.checks.listForRef({
owner,
repo,
ref: pr.head.sha,
per_page: 100,
});
const ciGateRuns = (checkRunsData.check_runs || [])
.filter((run) => run.name === "CI Required Gate")
.sort((a, b) => {
const aTime = new Date(a.started_at || a.completed_at || a.created_at).getTime();
const bTime = new Date(b.started_at || b.completed_at || b.created_at).getTime();
return bTime - aTime;
});
let ciState = "missing";
if (ciGateRuns.length > 0) {
const latest = ciGateRuns[0];
if (latest.status !== "completed") {
ciState = "in_progress";
} else if (["success", "neutral", "skipped"].includes(latest.conclusion || "")) {
ciState = "success";
} else {
ciState = String(latest.conclusion || "failure");
}
}
const ciMissing = ciState === "missing";
const ciFailing = !["success", "in_progress", "missing"].includes(ciState);
if (!isBehindBase && !ciMissing && !ciFailing) {
skipped += 1;
continue;
}
const reasons = [];
if (isBehindBase) {
reasons.push("- Branch is behind `main` (please rebase or merge the latest base branch).");
}
if (ciMissing) {
reasons.push("- No `CI Required Gate` run was found for the current head commit.");
}
if (ciFailing) {
reasons.push(`- Latest \`CI Required Gate\` result is \`${ciState}\`.`);
}
const shortSha = pr.head.sha.slice(0, 12);
const body = [
marker,
`Hi @${pr.user.login}, friendly automation nudge from PR hygiene.`,
"",
`This PR has had no new commits for **${Math.floor(ageHours)}h** and still needs an update before merge:`,
"",
...reasons,
"",
"### Recommended next steps",
"1. Rebase your branch on `main`.",
"2. Push the updated branch and re-run checks (or use **Re-run failed jobs**).",
"3. Post fresh validation output in this PR thread.",
"",
"Maintainers: apply `no-stale` to opt out for accepted-but-blocked work.",
`Head SHA: \`${shortSha}\``,
].join("\n");
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: pr.number,
per_page: 100,
});
const existing = comments.find(
(comment) => comment.user?.type === "Bot" && comment.body?.includes(marker),
);
if (existing) {
if (existing.body === body) {
skipped += 1;
continue;
}
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body,
});
}
nudged += 1;
core.info(`#${pr.number}: hygiene nudge posted/updated.`);
}
core.info(`Done. Nudged=${nudged}, skipped=${skipped}`);
};
@@ -1,204 +0,0 @@
// Run safe intake checks for PR events and maintain a single sticky comment.
// Used by .github/workflows/pr-intake-checks.yml via actions/github-script.
module.exports = async ({ github, context, core }) => {
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr = context.payload.pull_request;
if (!pr) return;
const prAuthor = (pr.user?.login || "").toLowerCase();
const prBaseRef = pr.base?.ref || "";
const marker = "<!-- pr-intake-checks -->";
const legacyMarker = "<!-- pr-intake-sanity -->";
const requiredSections = [
"## Summary",
"## Validation Evidence (required)",
"## Security Impact (required)",
"## Privacy and Data Hygiene (required)",
"## Rollback Plan (required)",
];
const body = pr.body || "";
const missingSections = requiredSections.filter((section) => !body.includes(section));
const missingFields = [];
const requiredFieldChecks = [
["summary problem", /- Problem:\s*\S+/m],
["summary why it matters", /- Why it matters:\s*\S+/m],
["summary what changed", /- What changed:\s*\S+/m],
["validation commands", /Commands and result summary:\s*[\s\S]*```/m],
["security risk/mitigation", /- New permissions\/capabilities\?\s*\(`Yes\/No`\):\s*\S+/m],
["privacy status", /- Data-hygiene status\s*\(`pass\|needs-follow-up`\):\s*\S+/m],
["rollback plan", /- Fast rollback command\/path:\s*\S+/m],
];
for (const [name, pattern] of requiredFieldChecks) {
if (!pattern.test(body)) {
missingFields.push(name);
}
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
});
const formatWarnings = [];
const dangerousProblems = [];
for (const file of files) {
const patch = file.patch || "";
if (!patch) continue;
const lines = patch.split("\n");
for (let idx = 0; idx < lines.length; idx += 1) {
const line = lines[idx];
if (!line.startsWith("+") || line.startsWith("+++")) continue;
const added = line.slice(1);
const lineNo = idx + 1;
if (/\t/.test(added)) {
formatWarnings.push(`${file.filename}:patch#${lineNo} contains tab characters`);
}
if (/[ \t]+$/.test(added)) {
formatWarnings.push(`${file.filename}:patch#${lineNo} contains trailing whitespace`);
}
if (/^(<<<<<<<|=======|>>>>>>>)/.test(added)) {
dangerousProblems.push(`${file.filename}:patch#${lineNo} contains merge conflict markers`);
}
}
}
const workflowFilesChanged = files
.map((file) => file.filename)
.filter((name) => name.startsWith(".github/workflows/"));
const advisoryFindings = [];
const blockingFindings = [];
if (missingSections.length > 0) {
advisoryFindings.push(`Missing required PR template sections: ${missingSections.join(", ")}`);
}
if (missingFields.length > 0) {
advisoryFindings.push(`Incomplete required PR template fields: ${missingFields.join(", ")}`);
}
if (formatWarnings.length > 0) {
advisoryFindings.push(`Formatting issues in added lines (${formatWarnings.length})`);
}
if (dangerousProblems.length > 0) {
blockingFindings.push(`Dangerous patch markers found (${dangerousProblems.length})`);
}
const promotionAuthorAllowlist = new Set(["willsarg", "theonlyhennygod"]);
const shouldRetargetToDev =
prBaseRef === "main" && !promotionAuthorAllowlist.has(prAuthor);
if (shouldRetargetToDev) {
advisoryFindings.push(
"This PR targets `main`, but normal contributions must target `dev`. Retarget this PR to `dev` unless this is an authorized promotion PR.",
);
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pr.number,
per_page: 100,
});
const existing = comments.find((comment) => {
const body = comment.body || "";
return body.includes(marker) || body.includes(legacyMarker);
});
if (advisoryFindings.length === 0 && blockingFindings.length === 0) {
if (existing) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existing.id,
});
}
core.info("PR intake sanity checks passed.");
return;
}
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const advisoryDetails = [];
if (formatWarnings.length > 0) {
advisoryDetails.push(...formatWarnings.slice(0, 20).map((entry) => `- ${entry}`));
if (formatWarnings.length > 20) {
advisoryDetails.push(`- ...and ${formatWarnings.length - 20} more issue(s)`);
}
}
const blockingDetails = [];
if (dangerousProblems.length > 0) {
blockingDetails.push(...dangerousProblems.slice(0, 20).map((entry) => `- ${entry}`));
if (dangerousProblems.length > 20) {
blockingDetails.push(`- ...and ${dangerousProblems.length - 20} more issue(s)`);
}
}
const isBlocking = blockingFindings.length > 0;
const ownerApprovalNote = workflowFilesChanged.length > 0
? [
"",
"Workflow files changed in this PR:",
...workflowFilesChanged.map((name) => `- \`${name}\``),
"",
"Reminder: workflow changes require owner approval via `CI Required Gate`.",
].join("\n")
: "";
const commentBody = [
marker,
isBlocking
? "### PR intake checks failed (blocking)"
: "### PR intake checks found warnings (non-blocking)",
"",
isBlocking
? "Fast safe checks found blocking safety issues:"
: "Fast safe checks found advisory issues. CI lint/test/build gates still enforce merge quality.",
...(blockingFindings.length > 0 ? blockingFindings.map((entry) => `- ${entry}`) : []),
...(advisoryFindings.length > 0 ? advisoryFindings.map((entry) => `- ${entry}`) : []),
"",
"Action items:",
"1. Complete required PR template sections/fields.",
"2. Remove tabs, trailing whitespace, and merge conflict markers from added lines.",
"3. Re-run local checks before pushing:",
" - `./scripts/ci/rust_quality_gate.sh`",
" - `./scripts/ci/rust_strict_delta_gate.sh`",
" - `./scripts/ci/docs_quality_gate.sh`",
...(shouldRetargetToDev
? ["4. Retarget this PR base branch from `main` to `dev`."]
: []),
"",
`Run logs: ${runUrl}`,
"",
"Detected blocking line issues (sample):",
...(blockingDetails.length > 0 ? blockingDetails : ["- none"]),
"",
"Detected advisory line issues (sample):",
...(advisoryDetails.length > 0 ? advisoryDetails : ["- none"]),
ownerApprovalNote,
].join("\n");
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: commentBody,
});
}
if (isBlocking) {
core.setFailed("PR intake sanity checks found blocking issues. See sticky comment for details.");
return;
}
core.info("PR intake sanity checks found advisory issues only.");
};
-805
View File
@@ -1,805 +0,0 @@
// Apply managed PR labels (size/risk/path/module/contributor tiers).
// Extracted from pr-labeler workflow inline github-script for maintainability.
module.exports = async ({ github, context, core }) => {
const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const action = context.payload.action;
const changedLabel = context.payload.label?.name;
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const computedRiskLabels = ["risk: low", "risk: medium", "risk: high"];
const manualRiskOverrideLabel = "risk: manual";
const managedEnforcedLabels = new Set([
...sizeLabels,
manualRiskOverrideLabel,
...computedRiskLabels,
]);
if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) {
core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`);
return;
}
async function loadContributorTierPolicy() {
const policyPath = process.env.LABEL_POLICY_PATH || ".github/label-policy.json";
const fallback = {
contributorTierColor: "2ED9FF",
contributorTierRules: [
{ label: "distinguished contributor", minMergedPRs: 50 },
{ label: "principal contributor", minMergedPRs: 20 },
{ label: "experienced contributor", minMergedPRs: 10 },
{ label: "trusted contributor", minMergedPRs: 5 },
],
};
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path: policyPath,
ref: context.payload.repository?.default_branch || "main",
});
const json = JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
const contributorTierRules = (json.contributor_tiers || []).map((entry) => ({
label: String(entry.label || "").trim(),
minMergedPRs: Number(entry.min_merged_prs || 0),
}));
const contributorTierColor = String(json.contributor_tier_color || "").toUpperCase();
if (!contributorTierColor || contributorTierRules.length === 0) {
return fallback;
}
return { contributorTierColor, contributorTierRules };
} catch (error) {
core.warning(`failed to load ${policyPath}, using fallback policy: ${error.message}`);
return fallback;
}
}
const { contributorTierColor, contributorTierRules } = await loadContributorTierPolicy();
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
const managedPathLabels = [
"docs",
"dependencies",
"ci",
"core",
"agent",
"channel",
"config",
"cron",
"daemon",
"doctor",
"gateway",
"health",
"heartbeat",
"integration",
"memory",
"observability",
"onboard",
"provider",
"runtime",
"security",
"service",
"skillforge",
"skills",
"tool",
"tunnel",
"tests",
"scripts",
"dev",
];
const managedPathLabelSet = new Set(managedPathLabels);
const moduleNamespaceRules = [
{ root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) },
{ root: "src/channels/", prefix: "channel", coreEntries: new Set(["mod.rs", "traits.rs"]) },
{ root: "src/config/", prefix: "config", coreEntries: new Set(["mod.rs", "schema.rs"]) },
{ root: "src/cron/", prefix: "cron", coreEntries: new Set(["mod.rs"]) },
{ root: "src/daemon/", prefix: "daemon", coreEntries: new Set(["mod.rs"]) },
{ root: "src/doctor/", prefix: "doctor", coreEntries: new Set(["mod.rs"]) },
{ root: "src/gateway/", prefix: "gateway", coreEntries: new Set(["mod.rs"]) },
{ root: "src/health/", prefix: "health", coreEntries: new Set(["mod.rs"]) },
{ root: "src/heartbeat/", prefix: "heartbeat", coreEntries: new Set(["mod.rs"]) },
{ root: "src/integrations/", prefix: "integration", coreEntries: new Set(["mod.rs", "registry.rs"]) },
{ root: "src/memory/", prefix: "memory", coreEntries: new Set(["mod.rs", "traits.rs"]) },
{ root: "src/observability/", prefix: "observability", coreEntries: new Set(["mod.rs", "traits.rs"]) },
{ root: "src/onboard/", prefix: "onboard", coreEntries: new Set(["mod.rs"]) },
{ root: "src/providers/", prefix: "provider", coreEntries: new Set(["mod.rs", "traits.rs"]) },
{ root: "src/runtime/", prefix: "runtime", coreEntries: new Set(["mod.rs", "traits.rs"]) },
{ root: "src/security/", prefix: "security", coreEntries: new Set(["mod.rs"]) },
{ root: "src/service/", prefix: "service", coreEntries: new Set(["mod.rs"]) },
{ root: "src/skillforge/", prefix: "skillforge", coreEntries: new Set(["mod.rs"]) },
{ root: "src/skills/", prefix: "skills", coreEntries: new Set(["mod.rs"]) },
{ root: "src/tools/", prefix: "tool", coreEntries: new Set(["mod.rs", "traits.rs"]) },
{ root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) },
];
const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))];
const orderedOtherLabelStyles = [
{ label: "health", color: "8EC9B8" },
{ label: "tool", color: "7FC4B6" },
{ label: "agent", color: "86C4A2" },
{ label: "memory", color: "8FCB99" },
{ label: "channel", color: "7EB6F2" },
{ label: "service", color: "95C7B6" },
{ label: "integration", color: "8DC9AE" },
{ label: "tunnel", color: "9FC8B3" },
{ label: "config", color: "AABCD0" },
{ label: "observability", color: "84C9D0" },
{ label: "docs", color: "8FBBE0" },
{ label: "dev", color: "B9C1CC" },
{ label: "tests", color: "9DC8C7" },
{ label: "skills", color: "BFC89B" },
{ label: "skillforge", color: "C9C39B" },
{ label: "provider", color: "958DF0" },
{ label: "runtime", color: "A3ADD8" },
{ label: "heartbeat", color: "C0C88D" },
{ label: "daemon", color: "C8C498" },
{ label: "doctor", color: "C1CF9D" },
{ label: "onboard", color: "D2BF86" },
{ label: "cron", color: "D2B490" },
{ label: "ci", color: "AEB4CE" },
{ label: "dependencies", color: "9FB1DE" },
{ label: "gateway", color: "B5A8E5" },
{ label: "security", color: "E58D85" },
{ label: "core", color: "C8A99B" },
{ label: "scripts", color: "C9B49F" },
];
const otherLabelDisplayOrder = orderedOtherLabelStyles.map((entry) => entry.label);
const modulePrefixSet = new Set(moduleNamespaceRules.map((rule) => rule.prefix));
const modulePrefixPriority = otherLabelDisplayOrder.filter((label) => modulePrefixSet.has(label));
const pathLabelPriority = [...otherLabelDisplayOrder];
const riskDisplayOrder = ["risk: high", "risk: medium", "risk: low", "risk: manual"];
const sizeDisplayOrder = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const contributorDisplayOrder = [
"distinguished contributor",
"principal contributor",
"experienced contributor",
"trusted contributor",
];
const modulePrefixPriorityIndex = new Map(
modulePrefixPriority.map((prefix, index) => [prefix, index])
);
const pathLabelPriorityIndex = new Map(
pathLabelPriority.map((label, index) => [label, index])
);
const riskPriorityIndex = new Map(
riskDisplayOrder.map((label, index) => [label, index])
);
const sizePriorityIndex = new Map(
sizeDisplayOrder.map((label, index) => [label, index])
);
const contributorPriorityIndex = new Map(
contributorDisplayOrder.map((label, index) => [label, index])
);
const otherLabelColors = Object.fromEntries(
orderedOtherLabelStyles.map((entry) => [entry.label, entry.color])
);
const staticLabelColors = {
"size: XS": "E7CDD3",
"size: S": "E1BEC7",
"size: M": "DBB0BB",
"size: L": "D4A2AF",
"size: XL": "CE94A4",
"risk: low": "97D3A6",
"risk: medium": "E4C47B",
"risk: high": "E98E88",
"risk: manual": "B7A4E0",
...otherLabelColors,
};
const staticLabelDescriptions = {
"size: XS": "Auto size: <=80 non-doc changed lines.",
"size: S": "Auto size: 81-250 non-doc changed lines.",
"size: M": "Auto size: 251-500 non-doc changed lines.",
"size: L": "Auto size: 501-1000 non-doc changed lines.",
"size: XL": "Auto size: >1000 non-doc changed lines.",
"risk: low": "Auto risk: docs/chore-only paths.",
"risk: medium": "Auto risk: src/** or dependency/config changes.",
"risk: high": "Auto risk: security/runtime/gateway/tools/workflows.",
"risk: manual": "Maintainer override: keep selected risk label.",
docs: "Auto scope: docs/markdown/template files changed.",
dependencies: "Auto scope: dependency manifest/lock/policy changed.",
ci: "Auto scope: CI/workflow/hook files changed.",
core: "Auto scope: root src/*.rs files changed.",
agent: "Auto scope: src/agent/** changed.",
channel: "Auto scope: src/channels/** changed.",
config: "Auto scope: src/config/** changed.",
cron: "Auto scope: src/cron/** changed.",
daemon: "Auto scope: src/daemon/** changed.",
doctor: "Auto scope: src/doctor/** changed.",
gateway: "Auto scope: src/gateway/** changed.",
health: "Auto scope: src/health/** changed.",
heartbeat: "Auto scope: src/heartbeat/** changed.",
integration: "Auto scope: src/integrations/** changed.",
memory: "Auto scope: src/memory/** changed.",
observability: "Auto scope: src/observability/** changed.",
onboard: "Auto scope: src/onboard/** changed.",
provider: "Auto scope: src/providers/** changed.",
runtime: "Auto scope: src/runtime/** changed.",
security: "Auto scope: src/security/** changed.",
service: "Auto scope: src/service/** changed.",
skillforge: "Auto scope: src/skillforge/** changed.",
skills: "Auto scope: src/skills/** changed.",
tool: "Auto scope: src/tools/** changed.",
tunnel: "Auto scope: src/tunnel/** changed.",
tests: "Auto scope: tests/** changed.",
scripts: "Auto scope: scripts/** changed.",
dev: "Auto scope: dev/** changed.",
};
for (const label of contributorTierLabels) {
staticLabelColors[label] = contributorTierColor;
const rule = contributorTierRules.find((entry) => entry.label === label);
if (rule) {
staticLabelDescriptions[label] = `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
}
}
const modulePrefixColors = Object.fromEntries(
modulePrefixPriority.map((prefix) => [
`${prefix}:`,
otherLabelColors[prefix] || "BFDADC",
])
);
const providerKeywordHints = [
"deepseek",
"moonshot",
"kimi",
"qwen",
"mistral",
"doubao",
"baichuan",
"yi",
"siliconflow",
"vertex",
"azure",
"perplexity",
"venice",
"vercel",
"cloudflare",
"synthetic",
"opencode",
"zai",
"glm",
"minimax",
"bedrock",
"qianfan",
"groq",
"together",
"fireworks",
"novita",
"cohere",
"openai",
"openrouter",
"anthropic",
"gemini",
"ollama",
];
const channelKeywordHints = [
"telegram",
"discord",
"slack",
"whatsapp",
"matrix",
"irc",
"imessage",
"email",
"cli",
];
function isDocsLike(path) {
return (
path.startsWith("docs/") ||
path.endsWith(".md") ||
path.endsWith(".mdx") ||
path === "LICENSE" ||
path === ".markdownlint-cli2.yaml" ||
path === ".github/pull_request_template.md" ||
path.startsWith(".github/ISSUE_TEMPLATE/")
);
}
function normalizeLabelSegment(segment) {
return (segment || "")
.toLowerCase()
.replace(/\.rs$/g, "")
.replace(/[^a-z0-9_-]+/g, "-")
.replace(/^[-_]+|[-_]+$/g, "")
.slice(0, 40);
}
function containsKeyword(text, keyword) {
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, "i");
return pattern.test(text);
}
function formatModuleLabel(prefix, segment) {
return `${prefix}: ${segment}`;
}
function parseModuleLabel(label) {
if (typeof label !== "string") return null;
const match = label.match(/^([^:]+):\s*(.+)$/);
if (!match) return null;
const prefix = match[1].trim().toLowerCase();
const segment = (match[2] || "").trim().toLowerCase();
if (!prefix || !segment) return null;
return { prefix, segment };
}
function sortByPriority(labels, priorityIndex) {
return [...new Set(labels)].sort((left, right) => {
const leftPriority = priorityIndex.has(left) ? priorityIndex.get(left) : Number.MAX_SAFE_INTEGER;
const rightPriority = priorityIndex.has(right)
? priorityIndex.get(right)
: Number.MAX_SAFE_INTEGER;
if (leftPriority !== rightPriority) return leftPriority - rightPriority;
return left.localeCompare(right);
});
}
function sortModuleLabels(labels) {
return [...new Set(labels)].sort((left, right) => {
const leftParsed = parseModuleLabel(left);
const rightParsed = parseModuleLabel(right);
if (!leftParsed || !rightParsed) return left.localeCompare(right);
const leftPrefixPriority = modulePrefixPriorityIndex.has(leftParsed.prefix)
? modulePrefixPriorityIndex.get(leftParsed.prefix)
: Number.MAX_SAFE_INTEGER;
const rightPrefixPriority = modulePrefixPriorityIndex.has(rightParsed.prefix)
? modulePrefixPriorityIndex.get(rightParsed.prefix)
: Number.MAX_SAFE_INTEGER;
if (leftPrefixPriority !== rightPrefixPriority) {
return leftPrefixPriority - rightPrefixPriority;
}
if (leftParsed.prefix !== rightParsed.prefix) {
return leftParsed.prefix.localeCompare(rightParsed.prefix);
}
const leftIsCore = leftParsed.segment === "core";
const rightIsCore = rightParsed.segment === "core";
if (leftIsCore !== rightIsCore) return leftIsCore ? 1 : -1;
return leftParsed.segment.localeCompare(rightParsed.segment);
});
}
function refineModuleLabels(rawLabels) {
const refined = new Set(rawLabels);
const segmentsByPrefix = new Map();
for (const label of rawLabels) {
const parsed = parseModuleLabel(label);
if (!parsed) continue;
if (!segmentsByPrefix.has(parsed.prefix)) {
segmentsByPrefix.set(parsed.prefix, new Set());
}
segmentsByPrefix.get(parsed.prefix).add(parsed.segment);
}
for (const [prefix, segments] of segmentsByPrefix) {
const hasSpecificSegment = [...segments].some((segment) => segment !== "core");
if (hasSpecificSegment) {
refined.delete(formatModuleLabel(prefix, "core"));
}
}
return refined;
}
function compactModuleLabels(labels) {
const groupedSegments = new Map();
const compactedModuleLabels = new Set();
const forcePathPrefixes = new Set();
for (const label of labels) {
const parsed = parseModuleLabel(label);
if (!parsed) {
compactedModuleLabels.add(label);
continue;
}
if (!groupedSegments.has(parsed.prefix)) {
groupedSegments.set(parsed.prefix, new Set());
}
groupedSegments.get(parsed.prefix).add(parsed.segment);
}
for (const [prefix, segments] of groupedSegments) {
const uniqueSegments = [...new Set([...segments].filter(Boolean))];
if (uniqueSegments.length === 0) continue;
if (uniqueSegments.length === 1) {
compactedModuleLabels.add(formatModuleLabel(prefix, uniqueSegments[0]));
} else {
forcePathPrefixes.add(prefix);
}
}
return {
moduleLabels: compactedModuleLabels,
forcePathPrefixes,
};
}
function colorForLabel(label) {
if (staticLabelColors[label]) return staticLabelColors[label];
const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix));
if (matchedPrefix) return modulePrefixColors[matchedPrefix];
return "BFDADC";
}
function descriptionForLabel(label) {
if (staticLabelDescriptions[label]) return staticLabelDescriptions[label];
const parsed = parseModuleLabel(label);
if (parsed) {
if (parsed.segment === "core") {
return `Auto module: ${parsed.prefix} core files changed.`;
}
return `Auto module: ${parsed.prefix}/${parsed.segment} changed.`;
}
return "Auto-managed label.";
}
async function ensureLabel(name, existing = null) {
const expectedColor = colorForLabel(name);
const expectedDescription = descriptionForLabel(name);
try {
const current = existing || (await github.rest.issues.getLabel({ owner, repo, name })).data;
const currentColor = (current.color || "").toUpperCase();
const currentDescription = (current.description || "").trim();
if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
await github.rest.issues.updateLabel({
owner,
repo,
name,
new_name: name,
color: expectedColor,
description: expectedDescription,
});
}
} catch (error) {
if (error.status !== 404) throw error;
await github.rest.issues.createLabel({
owner,
repo,
name,
color: expectedColor,
description: expectedDescription,
});
}
}
function isManagedLabel(label) {
if (label === manualRiskOverrideLabel) return true;
if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return true;
if (managedPathLabelSet.has(label)) return true;
if (contributorTierLabels.includes(label)) return true;
if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return true;
return false;
}
async function ensureManagedRepoLabelsMetadata() {
const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner,
repo,
per_page: 100,
});
for (const existingLabel of repoLabels) {
const labelName = existingLabel.name || "";
if (!isManagedLabel(labelName)) continue;
await ensureLabel(labelName, existingLabel);
}
}
function selectContributorTier(mergedCount) {
const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
return matchedTier ? matchedTier.label : null;
}
if (context.eventName === "workflow_dispatch") {
const mode = (context.payload.inputs?.mode || "audit").toLowerCase();
const shouldRepair = mode === "repair";
const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner,
repo,
per_page: 100,
});
let managedScanned = 0;
const drifts = [];
for (const existingLabel of repoLabels) {
const labelName = existingLabel.name || "";
if (!isManagedLabel(labelName)) continue;
managedScanned += 1;
const expectedColor = colorForLabel(labelName);
const expectedDescription = descriptionForLabel(labelName);
const currentColor = (existingLabel.color || "").toUpperCase();
const currentDescription = (existingLabel.description || "").trim();
if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
drifts.push({
name: labelName,
currentColor,
expectedColor,
currentDescription,
expectedDescription,
});
if (shouldRepair) {
await ensureLabel(labelName, existingLabel);
}
}
}
core.summary
.addHeading("Managed Label Governance", 2)
.addRaw(`Mode: ${shouldRepair ? "repair" : "audit"}`)
.addEOL()
.addRaw(`Managed labels scanned: ${managedScanned}`)
.addEOL()
.addRaw(`Drifts found: ${drifts.length}`)
.addEOL();
if (drifts.length > 0) {
const sample = drifts.slice(0, 30).map((entry) => [
entry.name,
`${entry.currentColor} -> ${entry.expectedColor}`,
`${entry.currentDescription || "(blank)"} -> ${entry.expectedDescription}`,
]);
core.summary.addTable([
[{ data: "Label", header: true }, { data: "Color", header: true }, { data: "Description", header: true }],
...sample,
]);
if (drifts.length > sample.length) {
core.summary
.addRaw(`Additional drifts not shown: ${drifts.length - sample.length}`)
.addEOL();
}
}
await core.summary.write();
if (!shouldRepair && drifts.length > 0) {
core.info(`Managed-label metadata drifts detected: ${drifts.length}. Re-run with mode=repair to auto-fix.`);
} else if (shouldRepair) {
core.info(`Managed-label metadata repair applied to ${drifts.length} labels.`);
} else {
core.info("No managed-label metadata drift detected.");
}
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
});
const detectedModuleLabels = new Set();
for (const file of files) {
const path = (file.filename || "").toLowerCase();
for (const rule of moduleNamespaceRules) {
if (!path.startsWith(rule.root)) continue;
const relative = path.slice(rule.root.length);
if (!relative) continue;
const first = relative.split("/")[0];
const firstStem = first.endsWith(".rs") ? first.slice(0, -3) : first;
let segment = firstStem;
if (rule.coreEntries.has(first) || rule.coreEntries.has(firstStem)) {
segment = "core";
}
segment = normalizeLabelSegment(segment);
if (!segment) continue;
detectedModuleLabels.add(formatModuleLabel(rule.prefix, segment));
}
}
const providerRelevantFiles = files.filter((file) => {
const path = file.filename || "";
return (
path.startsWith("src/providers/") ||
path.startsWith("src/integrations/") ||
path.startsWith("src/onboard/") ||
path.startsWith("src/config/")
);
});
if (providerRelevantFiles.length > 0) {
const searchableText = [
pr.title || "",
pr.body || "",
...providerRelevantFiles.map((file) => file.filename || ""),
...providerRelevantFiles.map((file) => file.patch || ""),
]
.join("\n")
.toLowerCase();
for (const keyword of providerKeywordHints) {
if (containsKeyword(searchableText, keyword)) {
detectedModuleLabels.add(formatModuleLabel("provider", keyword));
}
}
}
const channelRelevantFiles = files.filter((file) => {
const path = file.filename || "";
return (
path.startsWith("src/channels/") ||
path.startsWith("src/onboard/") ||
path.startsWith("src/config/")
);
});
if (channelRelevantFiles.length > 0) {
const searchableText = [
pr.title || "",
pr.body || "",
...channelRelevantFiles.map((file) => file.filename || ""),
...channelRelevantFiles.map((file) => file.patch || ""),
]
.join("\n")
.toLowerCase();
for (const keyword of channelKeywordHints) {
if (containsKeyword(searchableText, keyword)) {
detectedModuleLabels.add(formatModuleLabel("channel", keyword));
}
}
}
const refinedModuleLabels = refineModuleLabels(detectedModuleLabels);
const compactedModuleState = compactModuleLabels(refinedModuleLabels);
const selectedModuleLabels = compactedModuleState.moduleLabels;
const forcePathPrefixes = compactedModuleState.forcePathPrefixes;
const modulePrefixesWithLabels = new Set(
[...selectedModuleLabels]
.map((label) => parseModuleLabel(label)?.prefix)
.filter(Boolean)
);
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: pr.number,
});
const currentLabelNames = currentLabels.map((label) => label.name);
const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label));
const candidatePathLabels = new Set([...currentPathLabels, ...forcePathPrefixes]);
const dedupedPathLabels = [...candidatePathLabels].filter((label) => {
if (label === "core") return true;
if (forcePathPrefixes.has(label)) return true;
return !modulePrefixesWithLabels.has(label);
});
const excludedLockfiles = new Set(["Cargo.lock"]);
const changedLines = files.reduce((total, file) => {
const path = file.filename || "";
if (isDocsLike(path) || excludedLockfiles.has(path)) {
return total;
}
return total + (file.additions || 0) + (file.deletions || 0);
}, 0);
let sizeLabel = "size: XL";
if (changedLines <= 80) sizeLabel = "size: XS";
else if (changedLines <= 250) sizeLabel = "size: S";
else if (changedLines <= 500) sizeLabel = "size: M";
else if (changedLines <= 1000) sizeLabel = "size: L";
const hasHighRiskPath = files.some((file) => {
const path = file.filename || "";
return (
path.startsWith("src/security/") ||
path.startsWith("src/runtime/") ||
path.startsWith("src/gateway/") ||
path.startsWith("src/tools/") ||
path.startsWith(".github/workflows/")
);
});
const hasMediumRiskPath = files.some((file) => {
const path = file.filename || "";
return (
path.startsWith("src/") ||
path === "Cargo.toml" ||
path === "Cargo.lock" ||
path === "deny.toml" ||
path.startsWith(".githooks/")
);
});
let riskLabel = "risk: low";
if (hasHighRiskPath) {
riskLabel = "risk: high";
} else if (hasMediumRiskPath) {
riskLabel = "risk: medium";
}
await ensureManagedRepoLabelsMetadata();
const labelsToEnsure = new Set([
...sizeLabels,
...computedRiskLabels,
manualRiskOverrideLabel,
...managedPathLabels,
...contributorTierLabels,
...selectedModuleLabels,
]);
for (const label of labelsToEnsure) {
await ensureLabel(label);
}
let contributorTierLabel = null;
const authorLogin = pr.user?.login;
if (authorLogin && pr.user?.type !== "Bot") {
try {
const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr is:merged author:${authorLogin}`,
per_page: 1,
});
const mergedCount = mergedSearch.total_count || 0;
contributorTierLabel = selectContributorTier(mergedCount);
} catch (error) {
core.warning(`failed to compute contributor tier label: ${error.message}`);
}
}
const hasManualRiskOverride = currentLabelNames.includes(manualRiskOverrideLabel);
const keepNonManagedLabels = currentLabelNames.filter((label) => {
if (label === manualRiskOverrideLabel) return true;
if (contributorTierLabels.includes(label)) return false;
if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return false;
if (managedPathLabelSet.has(label)) return false;
if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false;
return true;
});
const manualRiskSelection =
currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel;
const moduleLabelList = sortModuleLabels([...selectedModuleLabels]);
const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : [];
const selectedRiskLabels = hasManualRiskOverride
? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex)
: sortByPriority([riskLabel], riskPriorityIndex);
const selectedSizeLabels = sortByPriority([sizeLabel], sizePriorityIndex);
const sortedContributorLabels = sortByPriority(contributorLabelList, contributorPriorityIndex);
const sortedPathLabels = sortByPriority(dedupedPathLabels, pathLabelPriorityIndex);
const sortedKeepNonManagedLabels = [...new Set(keepNonManagedLabels)].sort((left, right) =>
left.localeCompare(right)
);
const nextLabels = [
...new Set([
...selectedRiskLabels,
...selectedSizeLabels,
...sortedContributorLabels,
...moduleLabelList,
...sortedPathLabels,
...sortedKeepNonManagedLabels,
]),
];
await github.rest.issues.setLabels({
owner,
repo,
issue_number: pr.number,
labels: nextLabels,
});
};
@@ -1,57 +0,0 @@
// Extracted from test-benchmarks.yml step: Post benchmark summary on PR
module.exports = async ({ github, context, core }) => {
const fs = require('fs');
const output = fs.readFileSync('benchmark_output.txt', 'utf8');
// Extract Criterion result lines
const lines = output.split('\n').filter(l =>
l.includes('time:') || l.includes('change:') || l.includes('Performance')
);
if (lines.length === 0) {
core.info('No benchmark results to post.');
return;
}
const body = [
'## 📊 Benchmark Results',
'',
'```',
lines.join('\n'),
'```',
'',
'<details><summary>Full output</summary>',
'',
'```',
output.substring(0, 60000),
'```',
'</details>',
].join('\n');
// Find and update or create comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
});
const marker = '## 📊 Benchmark Results';
const existing = comments.find(c => c.body && c.body.startsWith(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body,
});
}
};
-57
View File
@@ -1,57 +0,0 @@
name: Sec Audit
on:
push:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "deny.toml"
pull_request:
branches: [dev, main]
paths:
- "Cargo.toml"
- "Cargo.lock"
- "src/**"
- "crates/**"
- "deny.toml"
schedule:
- cron: "0 6 * * 1" # Weekly on Monday 6am UTC
concurrency:
group: security-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write
actions: read
checks: write
env:
CARGO_TERM_COLOR: always
jobs:
audit:
name: Security Audit
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
deny:
name: License & Supply Chain
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
with:
command: check advisories licenses sources
-39
View File
@@ -1,39 +0,0 @@
name: Sec CodeQL
on:
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6am UTC
workflow_dispatch:
concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write
actions: read
jobs:
codeql:
name: CodeQL Analysis
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
languages: rust
config-file: ./.github/codeql/codeql-config.yml
- name: Set up Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Build
run: cargo build --workspace --all-targets
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
-185
View File
@@ -1,185 +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
jobs:
vorpal:
name: Vorpal Reviewdog Scan
runs-on: blacksmith-2vcpu-ubuntu-2404
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
View File
@@ -1,116 +0,0 @@
name: Sync Contributors
on:
workflow_dispatch:
schedule:
# Run every Sunday at 00:00 UTC
- cron: '0 0 * * 0'
concurrency:
group: update-notice-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
update-notice:
name: Update NOTICE with new contributors
runs-on: ubuntu-latest
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"
-50
View File
@@ -1,50 +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:
CARGO_TERM_COLOR: always
jobs:
benchmarks:
name: Criterion Benchmarks
runs-on: blacksmith-2vcpu-ubuntu-2404
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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 });
-30
View File
@@ -1,30 +0,0 @@
name: Test E2E
on:
push:
branches: [dev, main]
workflow_dispatch:
concurrency:
group: e2e-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
integration-tests:
name: Integration / E2E Tests
runs-on: blacksmith-2vcpu-ubuntu-2404
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 integration / E2E tests
run: cargo test --test agent_e2e --locked --verbose
-72
View File
@@ -1,72 +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:
CARGO_TERM_COLOR: always
jobs:
fuzz:
name: Fuzz (${{ matrix.target }})
runs-on: blacksmith-2vcpu-ubuntu-2404
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
-62
View File
@@ -1,62 +0,0 @@
name: Test Rust Build
on:
workflow_call:
inputs:
run_command:
description: "Shell command(s) to execute."
required: true
type: string
timeout_minutes:
description: "Job timeout in minutes."
required: false
default: 20
type: number
toolchain:
description: "Rust toolchain channel/version."
required: false
default: "stable"
type: string
components:
description: "Optional rustup components."
required: false
default: ""
type: string
targets:
description: "Optional rustup targets."
required: false
default: ""
type: string
use_cache:
description: "Whether to enable rust-cache."
required: false
default: true
type: boolean
permissions:
contents: read
jobs:
run:
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: ${{ inputs.timeout_minutes }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
toolchain: ${{ inputs.toolchain }}
components: ${{ inputs.components }}
targets: ${{ inputs.targets }}
- name: Restore Rust cache
if: inputs.use_cache
uses: useblacksmith/rust-cache@f53e7f127245d2a269b3d90879ccf259876842d5 # v3
- name: Run command
shell: bash
run: |
set -euo pipefail
${{ inputs.run_command }}
-64
View File
@@ -1,64 +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
jobs:
no-tabs:
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Fail on tabs in workflow files
shell: bash
run: |
set -euo pipefail
python - <<'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: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Lint GitHub workflows
uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
+2 -3
View File
@@ -225,10 +225,9 @@ When uncertain, classify as higher risk.
All contributors (human or agent) must follow the same collaboration flow:
- Create and work from a non-`main` branch.
- Create and work from a non-`master` branch.
- Commit changes to that branch with clear, scoped commit messages.
- Open a PR to `dev`; do not push directly to `dev` or `main`.
- `main` is reserved for release promotion PRs from `dev`.
- Open a PR to `master`; do not push directly to `master`.
- Wait for required checks and review outcomes before merging.
- Merge via PR controls (squash/rebase/merge as repository policy allows).
- Branch deletion after merge is optional; long-lived branches are allowed when intentionally maintained.
+2 -2
View File
@@ -225,9 +225,9 @@ When uncertain, classify as higher risk.
All contributors (human or agent) must follow the same collaboration flow:
- Create and work from a non-`main` branch.
- Create and work from a non-`master` branch.
- Commit changes to that branch with clear, scoped commit messages.
- Open a PR to `main`; do not push directly to `main`.
- Open a PR to `master`; do not push directly to `master`.
- Wait for required checks and review outcomes before merging.
- Merge via PR controls (squash/rebase/merge as repository policy allows).
- Branch deletion after merge is optional; long-lived branches are allowed when intentionally maintained.
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://x.com/willsarg617.
https://x.com/argenistherose.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+1 -1
View File
@@ -17,7 +17,7 @@ Welcome — contributions of all sizes are valued. If this is your first contrib
- Fork the repository and clone your fork
- Create a feature branch (`git checkout -b fix/my-change`)
- Make your changes and run `cargo fmt && cargo clippy && cargo test`
- Open a PR against `dev` using the PR template
- Open a PR against `master` using the PR template
4. **Start with Track A.** ZeroClaw uses three [collaboration tracks](#collaboration-tracks-risk-based) (A/B/C) based on risk. First-time contributors should target **Track A** (docs, tests, chore) — these require lighter review and are the fastest path to a merged PR.
+12 -19
View File
@@ -1025,23 +1025,23 @@ Skill installs are now gated by a built-in static security audit. `zeroclaw skil
```bash
cargo build # Dev build
cargo build --release # Release build (codegen-units=1, works on all devices including Raspberry Pi)
cargo build --profile release-fast # Faster build (codegen-units=8, requires 16GB+ RAM)
cargo build --release # Release build
cargo test # Run full test suite
cargo clippy --locked --all-targets -- -D clippy::correctness
cargo fmt # Format
# Run the SQLite vs Markdown benchmark
cargo test --test memory_comparison -- --nocapture
```
### Pre-push hook
### CI / CD
A git hook runs `cargo fmt --check`, `cargo clippy -- -D warnings`, and `cargo test` before every push. Enable it once:
Three workflows power the entire pipeline:
```bash
git config core.hooksPath .githooks
```
| Workflow | Trigger | What it does |
|----------|---------|--------------|
| **CI** | Pull request to `master` | `cargo test` + `cargo build --release` |
| **Beta Release** | Push (merge) to `master` | Builds multi-platform binaries, creates a GitHub prerelease tagged `vX.Y.Z-beta.<run>`, pushes Docker image to GHCR |
| **Promote Release** | Manual `workflow_dispatch` | Validates version against `Cargo.toml`, builds release artifacts, creates a stable GitHub release, pushes Docker `:latest` |
**Versioning:** Semantic versioning based on the `version` field in `Cargo.toml`. Every merge to `master` automatically produces a beta prerelease. To cut a stable release, bump `Cargo.toml`, merge, then trigger _Promote Release_ with the matching version.
**Release targets:** `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-pc-windows-msvc`.
### Build troubleshooting (Linux OpenSSL errors)
@@ -1055,12 +1055,6 @@ cargo install --path . --force --locked
ZeroClaw is configured to use `rustls` for HTTP/TLS dependencies; `--locked` keeps the transitive graph deterministic on fresh environments.
To skip the hook when you need a quick push during development:
```bash
git push --no-verify
```
## Collaboration & Docs
Start from the docs hub for a task-oriented map:
@@ -1086,7 +1080,6 @@ Core collaboration references:
- Contribution guide: [CONTRIBUTING.md](CONTRIBUTING.md)
- PR workflow policy: [docs/pr-workflow.md](docs/pr-workflow.md)
- Reviewer playbook (triage + deep review): [docs/reviewer-playbook.md](docs/reviewer-playbook.md)
- CI ownership and triage map: [docs/ci-map.md](docs/ci-map.md)
- Security disclosure policy: [SECURITY.md](SECURITY.md)
For deployment and runtime operations:
+43 -61
View File
@@ -1,38 +1,41 @@
# Actions Source Policy (Phase 1)
# Actions Source Policy
This document defines the current GitHub Actions source-control policy for this repository.
Phase 1 objective: lock down action sources with minimal disruption, before full SHA pinning.
## Current Policy
- Repository Actions permissions: enabled
- Allowed actions mode: selected
- SHA pinning required: false (deferred to Phase 2)
Selected allowlist patterns:
Selected allowlist (all actions currently used across CI, Beta Release, and Promote Release workflows):
- `actions/*` (covers `actions/cache`, `actions/checkout`, `actions/upload-artifact`, `actions/download-artifact`, and other first-party actions)
- `docker/*`
| Action | Used In | Purpose |
|--------|---------|---------|
| `actions/checkout@v4` | All workflows | Repository checkout |
| `actions/upload-artifact@v4` | release, promote-release | Upload build artifacts |
| `actions/download-artifact@v4` | release, promote-release | Download build artifacts for packaging |
| `dtolnay/rust-toolchain@stable` | All workflows | Install Rust toolchain (1.92.0) |
| `Swatinem/rust-cache@v2` | All workflows | Cargo build/dependency caching |
| `docker/setup-buildx-action@v3` | release, promote-release | Docker Buildx setup |
| `docker/login-action@v3` | release, promote-release | GHCR authentication |
| `docker/build-push-action@v6` | release, promote-release | Multi-platform Docker image build and push |
Equivalent allowlist patterns:
- `actions/*`
- `dtolnay/rust-toolchain@*`
- `DavidAnson/markdownlint-cli2-action@*`
- `lycheeverse/lychee-action@*`
- `EmbarkStudios/cargo-deny-action@*`
- `rustsec/audit-check@*`
- `rhysd/actionlint@*`
- `softprops/action-gh-release@*`
- `sigstore/cosign-installer@*`
- `Checkmarx/vorpal-reviewdog-github-action@*`
- `useblacksmith/*` (Blacksmith self-hosted runner infrastructure)
- `Swatinem/rust-cache@*`
- `docker/*`
## Change Control Export
## Workflows
Use these commands to export the current effective policy for audit/change control:
| Workflow | File | Trigger |
|----------|------|---------|
| CI | `.github/workflows/ci.yml` | Pull requests to `master` |
| Daily Beta Release | `.github/workflows/release.yml` | Daily schedule (08:00 UTC) + manual `workflow_dispatch` |
| Promote Release | `.github/workflows/promote-release.yml` | Manual `workflow_dispatch` |
```bash
gh api repos/zeroclaw-labs/zeroclaw/actions/permissions
gh api repos/zeroclaw-labs/zeroclaw/actions/permissions/selected-actions
```
## Change Control
Record each policy change with:
@@ -42,53 +45,32 @@ Record each policy change with:
- allowlist delta (added/removed patterns)
- rollback note
## Why This Phase
Use these commands to export the current effective policy:
- Reduces supply-chain risk from unreviewed marketplace actions.
- Preserves current CI/CD functionality with low migration overhead.
- Prepares for Phase 2 full SHA pinning without blocking active development.
```bash
gh api repos/zeroclaw-labs/zeroclaw/actions/permissions
gh api repos/zeroclaw-labs/zeroclaw/actions/permissions/selected-actions
```
## Agentic Workflow Guardrails
Because this repository has high agent-authored change volume:
## Guardrails
- Any PR that adds or changes `uses:` action sources must include an allowlist impact note.
- New third-party actions require explicit maintainer review before allowlisting.
- Expand allowlist only for verified missing actions; avoid broad wildcard exceptions.
- Keep rollback instructions in the PR description for Actions policy changes.
## Validation Checklist
## Change Log
After allowlist changes, validate:
1. `CI`
2. `Docker`
3. `Security Audit`
4. `Workflow Sanity`
5. `Release` (when safe to run)
Failure mode to watch for:
- `action is not allowed by policy`
If encountered, add only the specific trusted missing action, rerun, and document why.
Latest sweep notes:
- 2026-02-21: Added manual Vorpal reviewdog workflow for targeted secure-coding checks on supported file types
- Added allowlist pattern: `Checkmarx/vorpal-reviewdog-github-action@*`
- Workflow uses pinned source: `Checkmarx/vorpal-reviewdog-github-action@8cc292f337a2f1dea581b4f4bd73852e7becb50d` (v1.2.0)
- 2026-02-17: Rust dependency cache migrated from `Swatinem/rust-cache` to `useblacksmith/rust-cache`
- No new allowlist pattern required (`useblacksmith/*` already allowlisted)
- 2026-02-16: Hidden dependency discovered in `release.yml`: `sigstore/cosign-installer@...`
- Added allowlist pattern: `sigstore/cosign-installer@*`
- 2026-02-16: Blacksmith migration blocked workflow execution
- Added allowlist pattern: `useblacksmith/*` for self-hosted runner infrastructure
- Actions: `useblacksmith/setup-docker-builder@v1`, `useblacksmith/build-push-action@v2`
- 2026-02-17: Security audit reproducibility/freshness balance update
- Added allowlist pattern: `rustsec/audit-check@*`
- Replaced inline `cargo install cargo-audit` execution with pinned `rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998` in `security.yml`
- Supersedes floating-version proposal in #588 while keeping action source policy explicit
- 2026-03-05: Complete workflow overhaul — replaced 22 workflows with 3 (CI, Beta Release, Promote Release)
- Removed patterns no longer in use: `DavidAnson/markdownlint-cli2-action@*`, `lycheeverse/lychee-action@*`, `EmbarkStudios/cargo-deny-action@*`, `rustsec/audit-check@*`, `rhysd/actionlint@*`, `sigstore/cosign-installer@*`, `Checkmarx/vorpal-reviewdog-github-action@*`, `useblacksmith/*`
- Added: `Swatinem/rust-cache@*` (replaces `useblacksmith/*` rust-cache fork)
- Retained: `actions/*`, `dtolnay/rust-toolchain@*`, `softprops/action-gh-release@*`, `docker/*`
- 2026-03-05: CI build optimization — added mold linker, cargo-nextest, CARGO_INCREMENTAL=0
- sccache removed due to fragile GHA cache backend causing build failures
- 2026-03-07: Release pipeline overhaul
- Removed: `softprops/action-gh-release@*` (replaced with built-in `gh` CLI)
- Beta trigger changed from push-on-master to daily schedule + workflow_dispatch
- Added default-branch guard on beta workflow_dispatch
- Added build targets: `armv7-unknown-linux-gnueabihf`, `x86_64-apple-darwin` (cross-compiled from macos-14)
## Rollback
+7 -12
View File
@@ -2,7 +2,7 @@
This document explains what each GitHub workflow does, when it runs, and whether it should block merges.
For event-by-event delivery behavior across PR, merge, push, and release, see [`.github/workflows/main-branch-flow.md`](../.github/workflows/main-branch-flow.md).
For event-by-event delivery behavior across PR, merge, push, and release, see [`.github/workflows/master-branch-flow.md`](../.github/workflows/master-branch-flow.md).
## Merge-Blocking vs Optional
@@ -13,8 +13,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
- `.github/workflows/ci-run.yml` (`CI`)
- Purpose: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate on changed Rust lines, `test`, release build smoke) + docs quality checks when docs change (`markdownlint` blocks only issues on changed lines; link check scans only links added on changed lines)
- Additional behavior: for Rust-impacting PRs and pushes, `CI Required Gate` requires `lint` + `test` + `build` (no PR build-only bypass)
- Additional behavior: PRs that change `.github/workflows/**` require at least one approving review from a login in `WORKFLOW_OWNER_LOGINS` (repository variable fallback: `theonlyhennygod,willsarg`)
- Additional behavior: PRs that change root license files (`LICENSE-APACHE`, `LICENSE-MIT`) must be authored by `willsarg`
- Additional behavior: PRs that change `.github/workflows/**` require at least one approving review from a login in `WORKFLOW_OWNER_LOGINS` (repository variable fallback: `theonlyhennygod,jordanthejet`)
- Additional behavior: lint gates run before `test`/`build`; when lint/docs gates fail on PRs, CI posts an actionable feedback comment with failing gate names and local fix commands
- Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
@@ -22,13 +21,10 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
- Recommended for workflow-changing PRs
- `.github/workflows/pr-intake-checks.yml` (`PR Intake Checks`)
- Purpose: safe pre-CI PR checks (template completeness, added-line tabs/trailing-whitespace/conflict markers) with immediate sticky feedback comment
- `.github/workflows/main-promotion-gate.yml` (`Main Promotion Gate`)
- Purpose: enforce stable-branch policy by allowing only `dev` -> `main` PR promotion authored by `willsarg` or `theonlyhennygod`
### Non-Blocking but Important
- `.github/workflows/pub-docker-img.yml` (`Docker`)
- Purpose: PR Docker smoke check on `dev`/`main` PRs and publish images on tag pushes (`v*`) only
- Purpose: PR Docker smoke check on `master` PRs and publish images on tag pushes (`v*`) only
- `.github/workflows/sec-audit.yml` (`Security Audit`)
- Purpose: dependency advisories (`rustsec/audit-check`, pinned SHA) and policy/license checks (`cargo deny`)
- `.github/workflows/sec-codeql.yml` (`CodeQL Analysis`)
@@ -75,15 +71,14 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
## Trigger Map
- `CI`: push to `dev` and `main`, PRs to `dev` and `main`
- `Docker`: tag push (`v*`) for publish, matching PRs to `dev`/`main` for smoke build, manual dispatch for smoke only
- `CI`: push to `master`, PRs to `master`
- `Docker`: tag push (`v*`) for publish, matching PRs to `master` for smoke build, manual dispatch for smoke only
- `Release`: tag push (`v*`), weekly schedule (verification-only), manual dispatch (verification or publish)
- `Pub Homebrew Core`: manual dispatch only
- `Security Audit`: push to `dev` and `main`, PRs to `dev` and `main`, weekly schedule
- `Security Audit`: push to `master`, PRs to `master`, weekly schedule
- `Sec Vorpal Reviewdog`: manual dispatch only
- `Workflow Sanity`: PR/push when `.github/workflows/**`, `.github/*.yml`, or `.github/*.yaml` change
- `Main Promotion Gate`: PRs to `main` only; requires PR author `willsarg`/`theonlyhennygod` and head branch `dev` in the same repository
- `Dependabot`: all update PRs target `dev` (not `main`)
- `Dependabot`: all update PRs target `master`
- `PR Intake Checks`: `pull_request_target` on opened/reopened/synchronize/edited/ready_for_review
- `Label Policy Sanity`: PR/push when `.github/label-policy.json`, `.github/workflows/pr-labeler.yml`, or `.github/workflows/pr-auto-response.yml` changes
- `PR Labeler`: `pull_request_target` lifecycle events
+23 -13
View File
@@ -2,7 +2,7 @@
Tài liệu này giải thích từng GitHub workflow làm gì, khi nào chạy và liệu nó có nên chặn merge hay không.
Để biết hành vi phân phối theo từng sự kiện qua PR, merge, push và release, xem [`.github/workflows/main-branch-flow.md`](../../../.github/workflows/main-branch-flow.md).
Để biết hành vi phân phối theo từng sự kiện qua PR, merge, push và release, xem [`.github/workflows/master-branch-flow.md`](../../../.github/workflows/master-branch-flow.md).
## Chặn merge và Tùy chọn
@@ -12,7 +12,8 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
- `.github/workflows/ci-run.yml` (`CI`)
- Mục đích: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate trên các dòng Rust thay đổi, `test`, kiểm tra smoke release build) + kiểm tra chất lượng tài liệu khi tài liệu thay đổi (`markdownlint` chỉ chặn các vấn đề trên dòng thay đổi; link check chỉ quét các link mới được thêm trên dòng thay đổi)
- Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,willsarg`)
- Hành vi bổ sung: đối với PR và push ảnh hưởng Rust, `CI Required Gate` yêu cầu `lint` + `test` + `build` (không có shortcut chỉ build trên PR)
- Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,jordanthejet`)
- Hành vi bổ sung: lint gate chạy trước `test`/`build`; khi lint/docs gate thất bại trên PR, CI đăng comment phản hồi hành động được với tên gate thất bại và các lệnh sửa cục bộ
- Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
@@ -24,13 +25,19 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
### Quan trọng nhưng không chặn
- `.github/workflows/pub-docker-img.yml` (`Docker`)
- Mục đích: kiểm tra Docker smoke trên PR và publish image khi push lên `main` (các đường dẫn build-input), push tag (`v*`) và khi dispatch thủ công
- Mục đích: kiểm tra Docker smoke trên PR lên `master` và publish image khi push tag (`v*`) only
- `.github/workflows/sec-audit.yml` (`Security Audit`)
- Mục đích: advisory phụ thuộc (`rustsec/audit-check`, SHA được pin) và kiểm tra chính sách/giấy phép (`cargo deny`)
- `.github/workflows/sec-codeql.yml` (`CodeQL Analysis`)
- Mục đích: phân tích tĩnh theo lịch/thủ công để phát hiện vấn đề bảo mật
- `.github/workflows/sec-vorpal-reviewdog.yml` (`Sec Vorpal Reviewdog`)
- Mục đích: quét phản hồi secure-coding thủ công cho các file non-Rust được hỗ trợ (`.py`, `.js`, `.jsx`, `.ts`, `.tsx`) sử dụng annotation reviewdog
- Kiểm soát nhiễu: loại trừ các đường dẫn test/fixture phổ biến và pattern file test theo mặc định (`include_tests=false`)
- `.github/workflows/pub-release.yml` (`Release`)
- Mục đích: build release artifact ở chế độ xác minh (thủ công/theo lịch) và publish GitHub release khi push tag hoặc chế độ publish thủ công
- `.github/workflows/pub-homebrew-core.yml` (`Pub Homebrew Core`)
- Mục đích: luồng PR bump formula Homebrew core thủ công, do bot sở hữu cho các tagged release
- Bảo vệ: release tag phải khớp version `Cargo.toml`
- `.github/workflows/pr-label-policy-check.yml` (`Label Policy Sanity`)
- Mục đích: xác thực chính sách bậc contributor dùng chung trong `.github/label-policy.json` và đảm bảo các label workflow sử dụng chính sách đó
- `.github/workflows/test-rust-build.yml` (`Rust Reusable Job`)
@@ -65,17 +72,19 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
## Bản đồ Trigger
- `CI`: push lên `main`, PR lên `main`
- `Docker`: push lên `main` khi Docker build input thay đổi, push tag (`v*`), PR tương ứng, dispatch thủ công
- `CI`: push lên `master`, PR lên `master`
- `Docker`: push tag (`v*`) để publish, PR lên `master` tương ứng để smoke build, dispatch thủ công chỉ smoke
- `Release`: push tag (`v*`), lịch hàng tuần (chỉ xác minh), dispatch thủ công (xác minh hoặc publish)
- `Security Audit`: push lên `main`, PR lên `main`, lịch hàng tuần
- `Pub Homebrew Core`: dispatch thủ công only
- `Security Audit`: push lên `master`, PR lên `master`, lịch hàng tuần
- `Sec Vorpal Reviewdog`: dispatch thủ công only
- `Workflow Sanity`: PR/push khi `.github/workflows/**`, `.github/*.yml` hoặc `.github/*.yaml` thay đổi
- `PR Intake Checks`: `pull_request_target` khi opened/reopened/synchronize/edited/ready_for_review
- `Label Policy Sanity`: PR/push khi `.github/label-policy.json`, `.github/workflows/pr-labeler.yml` hoặc `.github/workflows/pr-auto-response.yml` thay đổi
- `PR Labeler`: sự kiện vòng đời `pull_request_target`
- `PR Auto Responder`: issue opened/labeled, `pull_request_target` opened/labeled
- `Stale PR Check`: lịch hàng ngày, dispatch thủ công
- `Dependabot`: cửa sổ bảo trì phụ thuộc hàng tuần
- `Dependabot`: tất cả PR cập nhật nhắm vào `master`
- `PR Hygiene`: lịch mỗi 12 giờ, dispatch thủ công
## Hướng dẫn triage nhanh
@@ -83,12 +92,13 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
1. `CI Required Gate` thất bại: bắt đầu với `.github/workflows/ci-run.yml`.
2. Docker thất bại trên PR: kiểm tra job `pr-smoke` trong `.github/workflows/pub-docker-img.yml`.
3. Release thất bại (tag/thủ công/theo lịch): kiểm tra `.github/workflows/pub-release.yml` và kết quả job `prepare`.
4. Security thất bại: kiểm tra `.github/workflows/sec-audit.yml``deny.toml`.
5. Lỗi cú pháp/lint workflow: kiểm tra `.github/workflows/workflow-sanity.yml`.
6. PR intake thất bại: kiểm tra comment sticky `.github/workflows/pr-intake-checks.yml` và run log.
7. Lỗi parity chính sách nhãn: kiểm tra `.github/workflows/pr-label-policy-check.yml`.
8. Lỗi tài liệu trong CI: kiểm tra log job `docs-quality` trong `.github/workflows/ci-run.yml`.
9. Lỗi strict delta lint trong CI: kiểm tra log job `lint-strict-delta` và so sánh với phạm vi diff `BASE_SHA`.
4. Lỗi publish formula Homebrew: kiểm tra output tóm tắt `.github/workflows/pub-homebrew-core.yml` và biến bot token/fork.
5. Security thất bại: kiểm tra `.github/workflows/sec-audit.yml``deny.toml`.
6. Lỗi cú pháp/lint workflow: kiểm tra `.github/workflows/workflow-sanity.yml`.
7. PR intake thất bại: kiểm tra comment sticky `.github/workflows/pr-intake-checks.yml` và run log.
8. Lỗi parity chính sách nhãn: kiểm tra `.github/workflows/pr-label-policy-check.yml`.
9. Lỗi tài liệu trong CI: kiểm tra log job `docs-quality` trong `.github/workflows/ci-run.yml`.
10. Lỗi strict delta lint trong CI: kiểm tra log job `lint-strict-delta` và so sánh với phạm vi diff `BASE_SHA`.
## Quy tắc bảo trì
+6 -3
View File
@@ -93,15 +93,17 @@ Tự động hóa hỗ trợ việc triage và bảo vệ, nhưng trách nhiệm
## 3. Cài đặt repository bắt buộc
Duy trì các quy tắc branch protection sau trên `main`:
Duy trì các quy tắc branch protection sau trên `master`:
- Yêu cầu status check trước khi merge.
- Yêu cầu check `CI Required Gate`.
- Yêu cầu review pull request trước khi merge.
- Yêu cầu review CODEOWNERS cho các đường dẫn được bảo vệ.
- Với `.github/workflows/**`, yêu cầu phê duyệt từ owner qua `CI Required Gate` (`WORKFLOW_OWNER_LOGINS`) và giới hạn quyền bypass branch/ruleset cho org owner.
- Danh sách workflow-owner mặc định được cấu hình qua biến repository `WORKFLOW_OWNER_LOGINS` (xem CODEOWNERS cho maintainer hiện tại).
- Hủy bỏ approval cũ khi có commit mới được đẩy lên.
- Hạn chế force-push trên các branch được bảo vệ.
- Tất cả PR của contributor nhắm trực tiếp vào `master`.
---
@@ -123,6 +125,7 @@ Duy trì các quy tắc branch protection sau trên `main`:
- `CI Required Gate` là merge gate.
- PR chỉ thay đổi tài liệu sử dụng fast-path và bỏ qua các Rust job nặng.
- PR không phải tài liệu phải vượt qua lint, test và kiểm tra smoke release build.
- PR ảnh hưởng Rust sử dụng cùng bộ gate bắt buộc như push lên `master` (không có shortcut chỉ build trên PR).
### 4.3 Bước C: Review
@@ -213,7 +216,7 @@ Chúng tôi **không** yêu cầu contributor định lượng quyền sở hữ
- Mục tiêu triage maintainer đầu tiên: trong vòng 48 giờ.
- Nếu PR bị chặn, maintainer để lại một checklist hành động được.
- Tự động hóa `stale` được dùng để giữ hàng đợi lành mạnh; maintainer có thể áp dụng `no-stale` khi cần.
- Tự động hóa `pr-hygiene` kiểm tra các PR mở mỗi 12 giờ và đăng nhắc nhở khi PR không có commit mới trong 48+ giờ và hoặc là đang tụt hậu so với `main` hoặc thiếu/thất bại `CI Required Gate` trên head commit.
- Tự động hóa `pr-hygiene` kiểm tra các PR mở mỗi 12 giờ và đăng nhắc nhở khi PR không có commit mới trong 48+ giờ và rơi vào một trong hai trường hợp: đang tụt hậu so với `master` hoặc thiếu/thất bại `CI Required Gate` trên head commit.
### 8.1 Kiểm soát ngân sách hàng đợi
@@ -270,7 +273,7 @@ Các thay đổi ở những khu vực này yêu cầu review chặt chẽ hơn
Nếu một PR đã merge gây ra hồi quy:
1. Revert PR ngay lập tức trên `main`.
1. Revert PR ngay lập tức trên `master`.
2. Mở issue theo dõi với phân tích nguyên nhân gốc.
3. Chỉ đưa lại bản sửa lỗi khi có test hồi quy.
+31 -10
View File
@@ -7,7 +7,7 @@ Cập nhật lần cuối: **2026-02-20**.
## Mục tiêu release
- Đảm bảo release có thể dự đoán và lặp lại.
- Chỉ publish từ code đã có trên `main`.
- Chỉ publish từ code đã có trên `master`.
- Xác minh các artifact đa nền tảng trước khi publish.
- Duy trì nhịp release đều đặn ngay cả khi PR volume cao.
@@ -22,6 +22,7 @@ Cập nhật lần cuối: **2026-02-20**.
Automation release nằm tại:
- `.github/workflows/pub-release.yml`
- `.github/workflows/pub-homebrew-core.yml` (PR formula Homebrew thủ công, do bot sở hữu)
Các chế độ:
@@ -33,24 +34,24 @@ Các guardrail ở chế độ publish:
- Tag phải khớp định dạng semver-like `vX.Y.Z[-suffix]`.
- Tag phải đã tồn tại trên origin.
- Commit của tag phải có thể truy vết được từ `origin/main`.
- Commit của tag phải có thể truy vết được từ `origin/master`.
- GHCR image tag tương ứng (`ghcr.io/<owner>/<repo>:<tag>`) phải sẵn sàng trước khi GitHub Release publish hoàn tất.
- Artifact được xác minh trước khi publish.
## Quy trình maintainer
### 1) Preflight trên `main`
### 1) Preflight trên `master`
1. Đảm bảo các required check đều xanh trên `main` mới nhất.
1. Đảm bảo các required check đều xanh trên `master` mới nhất.
2. Xác nhận không có sự cố ưu tiên cao hoặc regression đã biết nào đang mở.
3. Xác nhận các workflow installer và Docker đều khoẻ mạnh trên các commit `main` gần đây.
3. Xác nhận các workflow installer và Docker đều khoẻ mạnh trên các commit `master` gần đây.
### 2) Chạy verification build (không publish)
Chạy `Pub Release` thủ công:
- `publish_release`: `false`
- `release_ref`: `main`
- `release_ref`: `master`
Kết quả mong đợi:
@@ -60,7 +61,7 @@ Kết quả mong đợi:
### 3) Cut release tag
Từ một checkout cục bộ sạch đã sync với `origin/main`:
Từ một checkout cục bộ sạch đã sync với `origin/master`:
```bash
scripts/release/cut_release_tag.sh vX.Y.Z --push
@@ -69,7 +70,7 @@ scripts/release/cut_release_tag.sh vX.Y.Z --push
Script này đảm bảo:
- working tree sạch
- `HEAD == origin/main`
- `HEAD == origin/master`
- tag không bị trùng lặp
- định dạng tag semver-like
@@ -91,14 +92,34 @@ Kết quả publish mong đợi:
### 5) Xác minh sau release
1. Xác minh GitHub Release asset có thể tải xuống.
2. Xác minh GHCR tag cho phiên bản đã release `latest`.
2. Xác minh GHCR tag cho phiên bản đã release (`vX.Y.Z`) và tag SHA commit release (`sha-<12>`).
3. Xác minh các đường dẫn cài đặt phụ thuộc vào release asset (ví dụ tải xuống binary bootstrap).
### 6) Publish formula Homebrew Core (do bot sở hữu)
Chạy `Pub Homebrew Core` thủ công:
- `release_tag`: `vX.Y.Z`
- `dry_run`: `true` trước, sau đó `false`
Cài đặt repository bắt buộc cho non-dry-run:
- secret: `HOMEBREW_CORE_BOT_TOKEN` (token từ tài khoản bot chuyên dụng, không phải tài khoản maintainer cá nhân)
- variable: `HOMEBREW_CORE_BOT_FORK_REPO` (ví dụ `zeroclaw-release-bot/homebrew-core`)
- variable tùy chọn: `HOMEBREW_CORE_BOT_EMAIL`
Các guardrail workflow:
- release tag phải khớp version `Cargo.toml`
- URL nguồn và SHA256 của formula được cập nhật từ tagged tarball
- license formula được chuẩn hóa thành `Apache-2.0 OR MIT`
- PR được mở từ bot fork vào `Homebrew/homebrew-core:master`
## Đường dẫn khẩn cấp / khôi phục
Nếu release push tag thất bại sau khi artifact đã được xác minh:
1. Sửa vấn đề workflow hoặc packaging trên `main`.
1. Sửa vấn đề workflow hoặc packaging trên `master`.
2. Chạy lại `Pub Release` thủ công ở chế độ publish với:
- `publish_release=true`
- `release_tag=<existing tag>`
+6 -7
View File
@@ -93,18 +93,17 @@ Automation assists with triage and guardrails, but final merge accountability re
## 3. Required Repository Settings
Maintain these branch protection rules on `dev` and `main`:
Maintain these branch protection rules on `master`:
- Require status checks before merge.
- Require check `CI Required Gate`.
- Require pull request reviews before merge.
- Require CODEOWNERS review for protected paths.
- For `.github/workflows/**`, require owner approval via `CI Required Gate` (`WORKFLOW_OWNER_LOGINS`) and keep branch/ruleset bypass limited to org owners.
- Default workflow-owner allowlist includes `theonlyhennygod`, `willsarg`, and `chumyin` (plus any comma-separated additions from `WORKFLOW_OWNER_LOGINS`).
- Default workflow-owner allowlist is configured via the `WORKFLOW_OWNER_LOGINS` repository variable (see CODEOWNERS for current maintainers).
- Dismiss stale approvals when new commits are pushed.
- Restrict force-push on protected branches.
- Route normal contributor PRs to `dev`.
- Allow `main` merges only through a promotion PR from `dev` (enforced by `Main Promotion Gate`).
- All contributor PRs target `master` directly.
---
@@ -126,7 +125,7 @@ Maintain these branch protection rules on `dev` and `main`:
- `CI Required Gate` is the merge gate.
- Docs-only PRs use fast-path and skip heavy Rust jobs.
- Non-doc PRs must pass lint, tests, and release build smoke check.
- Rust-impacting PRs use the same required gate set as `dev`/`main` pushes (no PR build-only shortcut).
- Rust-impacting PRs use the same required gate set as `master` pushes (no PR build-only shortcut).
### 4.3 Step C: Review
@@ -217,7 +216,7 @@ We do **not** require contributors to quantify AI-vs-human line ownership.
- First maintainer triage target: within 48 hours.
- If PR is blocked, maintainer leaves one actionable checklist.
- `stale` automation is used to keep queue healthy; maintainers can apply `no-stale` when needed.
- `pr-hygiene` automation checks open PRs every 12 hours and posts a nudge when a PR has no new commits for 48+ hours and is either behind `main` or missing/failing `CI Required Gate` on the head commit.
- `pr-hygiene` automation checks open PRs every 12 hours and posts a nudge when a PR has no new commits for 48+ hours and is either behind `master` or missing/failing `CI Required Gate` on the head commit.
### 8.1 Queue budget controls
@@ -274,7 +273,7 @@ For agent-assisted contributions, reviewers should also verify the author demons
If a merged PR causes regressions:
1. Revert PR immediately on `main`.
1. Revert PR immediately on `master`.
2. Open a follow-up issue with root-cause analysis.
3. Re-introduce fix only with regression tests.
+9 -9
View File
@@ -7,7 +7,7 @@ Last verified: **February 21, 2026**.
## Release Goals
- Keep releases predictable and repeatable.
- Publish only from code already in `main`.
- Publish only from code already in `master`.
- Verify multi-target artifacts before publish.
- Keep release cadence regular even with high PR volume.
@@ -34,24 +34,24 @@ Publish-mode guardrails:
- Tag must match semver-like format `vX.Y.Z[-suffix]`.
- Tag must already exist on origin.
- Tag commit must be reachable from `origin/main`.
- Tag commit must be reachable from `origin/master`.
- Matching GHCR image tag (`ghcr.io/<owner>/<repo>:<tag>`) must be available before GitHub Release publish completes.
- Artifacts are verified before publish.
## Maintainer Procedure
### 1) Preflight on `main`
### 1) Preflight on `master`
1. Ensure required checks are green on latest `main`.
1. Ensure required checks are green on latest `master`.
2. Confirm no high-priority incidents or known regressions are open.
3. Confirm installer and Docker workflows are healthy on recent `main` commits.
3. Confirm installer and Docker workflows are healthy on recent `master` commits.
### 2) Run verification build (no publish)
Run `Pub Release` manually:
- `publish_release`: `false`
- `release_ref`: `main`
- `release_ref`: `master`
Expected outcome:
@@ -61,7 +61,7 @@ Expected outcome:
### 3) Cut release tag
From a clean local checkout synced to `origin/main`:
From a clean local checkout synced to `origin/master`:
```bash
scripts/release/cut_release_tag.sh vX.Y.Z --push
@@ -70,7 +70,7 @@ scripts/release/cut_release_tag.sh vX.Y.Z --push
This script enforces:
- clean working tree
- `HEAD == origin/main`
- `HEAD == origin/master`
- non-duplicate tag
- semver-like tag format
@@ -119,7 +119,7 @@ Workflow guardrails:
If tag-push release fails after artifacts are validated:
1. Fix workflow or packaging issue on `main`.
1. Fix workflow or packaging issue on `master`.
2. Re-run manual `Pub Release` in publish mode with:
- `publish_release=true`
- `release_tag=<existing tag>`
+23 -13
View File
@@ -2,7 +2,7 @@
Tài liệu này giải thích từng GitHub workflow làm gì, khi nào chạy và liệu nó có nên chặn merge hay không.
Để biết hành vi phân phối theo từng sự kiện qua PR, merge, push và release, xem [`.github/workflows/main-branch-flow.md`](../../.github/workflows/main-branch-flow.md).
Để biết hành vi phân phối theo từng sự kiện qua PR, merge, push và release, xem [`.github/workflows/master-branch-flow.md`](../../.github/workflows/master-branch-flow.md).
## Chặn merge và Tùy chọn
@@ -12,7 +12,8 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
- `.github/workflows/ci-run.yml` (`CI`)
- Mục đích: Rust validation (`cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D clippy::correctness`, strict delta lint gate trên các dòng Rust thay đổi, `test`, kiểm tra smoke release build) + kiểm tra chất lượng tài liệu khi tài liệu thay đổi (`markdownlint` chỉ chặn các vấn đề trên dòng thay đổi; link check chỉ quét các link mới được thêm trên dòng thay đổi)
- Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,willsarg`)
- Hành vi bổ sung: đối với PR và push ảnh hưởng Rust, `CI Required Gate` yêu cầu `lint` + `test` + `build` (không có shortcut chỉ build trên PR)
- Hành vi bổ sung: các PR thay đổi `.github/workflows/**` yêu cầu ít nhất một review phê duyệt từ login trong `WORKFLOW_OWNER_LOGINS` (fallback biến repository: `theonlyhennygod,jordanthejet`)
- Hành vi bổ sung: lint gate chạy trước `test`/`build`; khi lint/docs gate thất bại trên PR, CI đăng comment phản hồi hành động được với tên gate thất bại và các lệnh sửa cục bộ
- Merge gate: `CI Required Gate`
- `.github/workflows/workflow-sanity.yml` (`Workflow Sanity`)
@@ -24,13 +25,19 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
### Quan trọng nhưng không chặn
- `.github/workflows/pub-docker-img.yml` (`Docker`)
- Mục đích: kiểm tra Docker smoke trên PR và publish image khi push lên `main` (các đường dẫn build-input), push tag (`v*`) và khi dispatch thủ công
- Mục đích: kiểm tra Docker smoke trên PR lên `master` và publish image khi push tag (`v*`) only
- `.github/workflows/sec-audit.yml` (`Security Audit`)
- Mục đích: advisory phụ thuộc (`rustsec/audit-check`, SHA được pin) và kiểm tra chính sách/giấy phép (`cargo deny`)
- `.github/workflows/sec-codeql.yml` (`CodeQL Analysis`)
- Mục đích: phân tích tĩnh theo lịch/thủ công để phát hiện vấn đề bảo mật
- `.github/workflows/sec-vorpal-reviewdog.yml` (`Sec Vorpal Reviewdog`)
- Mục đích: quét phản hồi secure-coding thủ công cho các file non-Rust được hỗ trợ (`.py`, `.js`, `.jsx`, `.ts`, `.tsx`) sử dụng annotation reviewdog
- Kiểm soát nhiễu: loại trừ các đường dẫn test/fixture phổ biến và pattern file test theo mặc định (`include_tests=false`)
- `.github/workflows/pub-release.yml` (`Release`)
- Mục đích: build release artifact ở chế độ xác minh (thủ công/theo lịch) và publish GitHub release khi push tag hoặc chế độ publish thủ công
- `.github/workflows/pub-homebrew-core.yml` (`Pub Homebrew Core`)
- Mục đích: luồng PR bump formula Homebrew core thủ công, do bot sở hữu cho các tagged release
- Bảo vệ: release tag phải khớp version `Cargo.toml`
- `.github/workflows/pr-label-policy-check.yml` (`Label Policy Sanity`)
- Mục đích: xác thực chính sách bậc contributor dùng chung trong `.github/label-policy.json` và đảm bảo các label workflow sử dụng chính sách đó
- `.github/workflows/test-rust-build.yml` (`Rust Reusable Job`)
@@ -65,17 +72,19 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
## Bản đồ Trigger
- `CI`: push lên `main`, PR lên `main`
- `Docker`: push lên `main` khi Docker build input thay đổi, push tag (`v*`), PR tương ứng, dispatch thủ công
- `CI`: push lên `master`, PR lên `master`
- `Docker`: push tag (`v*`) để publish, PR lên `master` tương ứng để smoke build, dispatch thủ công chỉ smoke
- `Release`: push tag (`v*`), lịch hàng tuần (chỉ xác minh), dispatch thủ công (xác minh hoặc publish)
- `Security Audit`: push lên `main`, PR lên `main`, lịch hàng tuần
- `Pub Homebrew Core`: dispatch thủ công only
- `Security Audit`: push lên `master`, PR lên `master`, lịch hàng tuần
- `Sec Vorpal Reviewdog`: dispatch thủ công only
- `Workflow Sanity`: PR/push khi `.github/workflows/**`, `.github/*.yml` hoặc `.github/*.yaml` thay đổi
- `PR Intake Checks`: `pull_request_target` khi opened/reopened/synchronize/edited/ready_for_review
- `Label Policy Sanity`: PR/push khi `.github/label-policy.json`, `.github/workflows/pr-labeler.yml` hoặc `.github/workflows/pr-auto-response.yml` thay đổi
- `PR Labeler`: sự kiện vòng đời `pull_request_target`
- `PR Auto Responder`: issue opened/labeled, `pull_request_target` opened/labeled
- `Stale PR Check`: lịch hàng ngày, dispatch thủ công
- `Dependabot`: cửa sổ bảo trì phụ thuộc hàng tuần
- `Dependabot`: tất cả PR cập nhật nhắm vào `master`
- `PR Hygiene`: lịch mỗi 12 giờ, dispatch thủ công
## Hướng dẫn triage nhanh
@@ -83,12 +92,13 @@ Các kiểm tra chặn merge nên giữ nhỏ và mang tính quyết định. C
1. `CI Required Gate` thất bại: bắt đầu với `.github/workflows/ci-run.yml`.
2. Docker thất bại trên PR: kiểm tra job `pr-smoke` trong `.github/workflows/pub-docker-img.yml`.
3. Release thất bại (tag/thủ công/theo lịch): kiểm tra `.github/workflows/pub-release.yml` và kết quả job `prepare`.
4. Security thất bại: kiểm tra `.github/workflows/sec-audit.yml``deny.toml`.
5. Lỗi cú pháp/lint workflow: kiểm tra `.github/workflows/workflow-sanity.yml`.
6. PR intake thất bại: kiểm tra comment sticky `.github/workflows/pr-intake-checks.yml` và run log.
7. Lỗi parity chính sách nhãn: kiểm tra `.github/workflows/pr-label-policy-check.yml`.
8. Lỗi tài liệu trong CI: kiểm tra log job `docs-quality` trong `.github/workflows/ci-run.yml`.
9. Lỗi strict delta lint trong CI: kiểm tra log job `lint-strict-delta` và so sánh với phạm vi diff `BASE_SHA`.
4. Lỗi publish formula Homebrew: kiểm tra output tóm tắt `.github/workflows/pub-homebrew-core.yml` và biến bot token/fork.
5. Security thất bại: kiểm tra `.github/workflows/sec-audit.yml``deny.toml`.
6. Lỗi cú pháp/lint workflow: kiểm tra `.github/workflows/workflow-sanity.yml`.
7. PR intake thất bại: kiểm tra comment sticky `.github/workflows/pr-intake-checks.yml` và run log.
8. Lỗi parity chính sách nhãn: kiểm tra `.github/workflows/pr-label-policy-check.yml`.
9. Lỗi tài liệu trong CI: kiểm tra log job `docs-quality` trong `.github/workflows/ci-run.yml`.
10. Lỗi strict delta lint trong CI: kiểm tra log job `lint-strict-delta` và so sánh với phạm vi diff `BASE_SHA`.
## Quy tắc bảo trì
+6 -3
View File
@@ -93,15 +93,17 @@ Tự động hóa hỗ trợ việc triage và bảo vệ, nhưng trách nhiệm
## 3. Cài đặt repository bắt buộc
Duy trì các quy tắc branch protection sau trên `main`:
Duy trì các quy tắc branch protection sau trên `master`:
- Yêu cầu status check trước khi merge.
- Yêu cầu check `CI Required Gate`.
- Yêu cầu review pull request trước khi merge.
- Yêu cầu review CODEOWNERS cho các đường dẫn được bảo vệ.
- Với `.github/workflows/**`, yêu cầu phê duyệt từ owner qua `CI Required Gate` (`WORKFLOW_OWNER_LOGINS`) và giới hạn quyền bypass branch/ruleset cho org owner.
- Danh sách workflow-owner mặc định được cấu hình qua biến repository `WORKFLOW_OWNER_LOGINS` (xem CODEOWNERS cho maintainer hiện tại).
- Hủy bỏ approval cũ khi có commit mới được đẩy lên.
- Hạn chế force-push trên các branch được bảo vệ.
- Tất cả PR của contributor nhắm trực tiếp vào `master`.
---
@@ -123,6 +125,7 @@ Duy trì các quy tắc branch protection sau trên `main`:
- `CI Required Gate` là merge gate.
- PR chỉ thay đổi tài liệu sử dụng fast-path và bỏ qua các Rust job nặng.
- PR không phải tài liệu phải vượt qua lint, test và kiểm tra smoke release build.
- PR ảnh hưởng Rust sử dụng cùng bộ gate bắt buộc như push lên `master` (không có shortcut chỉ build trên PR).
### 4.3 Bước C: Review
@@ -213,7 +216,7 @@ Chúng tôi **không** yêu cầu contributor định lượng quyền sở hữ
- Mục tiêu triage maintainer đầu tiên: trong vòng 48 giờ.
- Nếu PR bị chặn, maintainer để lại một checklist hành động được.
- Tự động hóa `stale` được dùng để giữ hàng đợi lành mạnh; maintainer có thể áp dụng `no-stale` khi cần.
- Tự động hóa `pr-hygiene` kiểm tra các PR mở mỗi 12 giờ và đăng nhắc nhở khi PR không có commit mới trong 48+ giờ và hoặc là đang tụt hậu so với `main` hoặc thiếu/thất bại `CI Required Gate` trên head commit.
- Tự động hóa `pr-hygiene` kiểm tra các PR mở mỗi 12 giờ và đăng nhắc nhở khi PR không có commit mới trong 48+ giờ và rơi vào một trong hai trường hợp: đang tụt hậu so với `master` hoặc thiếu/thất bại `CI Required Gate` trên head commit.
### 8.1 Kiểm soát ngân sách hàng đợi
@@ -270,7 +273,7 @@ Các thay đổi ở những khu vực này yêu cầu review chặt chẽ hơn
Nếu một PR đã merge gây ra hồi quy:
1. Revert PR ngay lập tức trên `main`.
1. Revert PR ngay lập tức trên `master`.
2. Mở issue theo dõi với phân tích nguyên nhân gốc.
3. Chỉ đưa lại bản sửa lỗi khi có test hồi quy.
+31 -10
View File
@@ -7,7 +7,7 @@ Cập nhật lần cuối: **2026-02-20**.
## Mục tiêu release
- Đảm bảo release có thể dự đoán và lặp lại.
- Chỉ publish từ code đã có trên `main`.
- Chỉ publish từ code đã có trên `master`.
- Xác minh các artifact đa nền tảng trước khi publish.
- Duy trì nhịp release đều đặn ngay cả khi PR volume cao.
@@ -22,6 +22,7 @@ Cập nhật lần cuối: **2026-02-20**.
Automation release nằm tại:
- `.github/workflows/pub-release.yml`
- `.github/workflows/pub-homebrew-core.yml` (PR formula Homebrew thủ công, do bot sở hữu)
Các chế độ:
@@ -33,24 +34,24 @@ Các guardrail ở chế độ publish:
- Tag phải khớp định dạng semver-like `vX.Y.Z[-suffix]`.
- Tag phải đã tồn tại trên origin.
- Commit của tag phải có thể truy vết được từ `origin/main`.
- Commit của tag phải có thể truy vết được từ `origin/master`.
- GHCR image tag tương ứng (`ghcr.io/<owner>/<repo>:<tag>`) phải sẵn sàng trước khi GitHub Release publish hoàn tất.
- Artifact được xác minh trước khi publish.
## Quy trình maintainer
### 1) Preflight trên `main`
### 1) Preflight trên `master`
1. Đảm bảo các required check đều xanh trên `main` mới nhất.
1. Đảm bảo các required check đều xanh trên `master` mới nhất.
2. Xác nhận không có sự cố ưu tiên cao hoặc regression đã biết nào đang mở.
3. Xác nhận các workflow installer và Docker đều khoẻ mạnh trên các commit `main` gần đây.
3. Xác nhận các workflow installer và Docker đều khoẻ mạnh trên các commit `master` gần đây.
### 2) Chạy verification build (không publish)
Chạy `Pub Release` thủ công:
- `publish_release`: `false`
- `release_ref`: `main`
- `release_ref`: `master`
Kết quả mong đợi:
@@ -60,7 +61,7 @@ Kết quả mong đợi:
### 3) Cut release tag
Từ một checkout cục bộ sạch đã sync với `origin/main`:
Từ một checkout cục bộ sạch đã sync với `origin/master`:
```bash
scripts/release/cut_release_tag.sh vX.Y.Z --push
@@ -69,7 +70,7 @@ scripts/release/cut_release_tag.sh vX.Y.Z --push
Script này đảm bảo:
- working tree sạch
- `HEAD == origin/main`
- `HEAD == origin/master`
- tag không bị trùng lặp
- định dạng tag semver-like
@@ -91,14 +92,34 @@ Kết quả publish mong đợi:
### 5) Xác minh sau release
1. Xác minh GitHub Release asset có thể tải xuống.
2. Xác minh GHCR tag cho phiên bản đã release `latest`.
2. Xác minh GHCR tag cho phiên bản đã release (`vX.Y.Z`) và tag SHA commit release (`sha-<12>`).
3. Xác minh các đường dẫn cài đặt phụ thuộc vào release asset (ví dụ tải xuống binary bootstrap).
### 6) Publish formula Homebrew Core (do bot sở hữu)
Chạy `Pub Homebrew Core` thủ công:
- `release_tag`: `vX.Y.Z`
- `dry_run`: `true` trước, sau đó `false`
Cài đặt repository bắt buộc cho non-dry-run:
- secret: `HOMEBREW_CORE_BOT_TOKEN` (token từ tài khoản bot chuyên dụng, không phải tài khoản maintainer cá nhân)
- variable: `HOMEBREW_CORE_BOT_FORK_REPO` (ví dụ `zeroclaw-release-bot/homebrew-core`)
- variable tùy chọn: `HOMEBREW_CORE_BOT_EMAIL`
Các guardrail workflow:
- release tag phải khớp version `Cargo.toml`
- URL nguồn và SHA256 của formula được cập nhật từ tagged tarball
- license formula được chuẩn hóa thành `Apache-2.0 OR MIT`
- PR được mở từ bot fork vào `Homebrew/homebrew-core:master`
## Đường dẫn khẩn cấp / khôi phục
Nếu release push tag thất bại sau khi artifact đã được xác minh:
1. Sửa vấn đề workflow hoặc packaging trên `main`.
1. Sửa vấn đề workflow hoặc packaging trên `master`.
2. Chạy lại `Pub Release` thủ công ở chế độ publish với:
- `publish_release=true`
- `release_tag=<existing tag>`
+3 -3
View File
@@ -58,7 +58,7 @@ Examples:
./bootstrap.sh --docker
# Remote one-liner
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/main/scripts/bootstrap.sh | bash
curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/scripts/bootstrap.sh | bash
Environment:
ZEROCLAW_CONTAINER_CLI Container CLI command (default: docker; auto-fallback: podman)
@@ -905,8 +905,8 @@ if [[ ! -f "$WORK_DIR/Cargo.toml" ]]; then
fi
TEMP_DIR="$(mktemp -d -t zeroclaw-bootstrap-XXXXXX)"
info "No local repository detected; cloning latest main branch"
git clone --depth 1 "$REPO_URL" "$TEMP_DIR"
info "No local repository detected; cloning latest master branch"
git clone --depth 1 --branch master "$REPO_URL" "$TEMP_DIR"
WORK_DIR="$TEMP_DIR"
TEMP_CLONE=true
fi
+2 -1
View File
@@ -1263,7 +1263,8 @@ mod tests {
assert!(
err.contains("credentials not set")
|| err.contains("169.254.169.254")
|| err.to_lowercase().contains("credential"),
|| err.to_lowercase().contains("credential")
|| err.to_lowercase().contains("builder error"),
"Expected missing-credentials style error, got: {err}"
);
}