diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index cd66646ec..d637d93c8 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -225,6 +225,32 @@ jobs: if-no-files-found: ignore retention-days: 14 + restricted-hermetic: + name: Restricted Hermetic Validation + needs: [changes] + if: needs.changes.outputs.rust_changed == 'true' + runs-on: [self-hosted, Linux, X64, aws-india, blacksmith-2vcpu-ubuntu-2404, hetzner] + timeout-minutes: 45 + env: + CARGO_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/cargo + RUSTUP_HOME: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/rustup + CARGO_TARGET_DIR: ${{ github.workspace }}/.ci-rust/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}/target + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Self-heal Rust toolchain cache + shell: bash + run: ./scripts/ci/self_heal_rust_toolchain.sh 1.92.0 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.92.0 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v3 + with: + prefix-key: ci-run-restricted-hermetic + cache-bin: false + - name: Run restricted-profile hermetic subset + shell: bash + run: ./scripts/ci/restricted_profile.sh + build: name: Build (Smoke) needs: [changes] @@ -501,7 +527,7 @@ jobs: ci-required: name: CI Required Gate if: always() - needs: [changes, lint, workspace-check, package-check, test, build, cross-platform-vm, linux-distro-container, docker-smoke, docs-only, non-rust, docs-quality, lint-feedback, license-file-owner-guard] + needs: [changes, lint, workspace-check, package-check, test, restricted-hermetic, build, cross-platform-vm, linux-distro-container, docker-smoke, docs-only, non-rust, docs-quality, lint-feedback, license-file-owner-guard] runs-on: [self-hosted, Linux, X64, aws-india, light, cpu40] steps: - name: Enforce required status @@ -558,6 +584,7 @@ jobs: workspace_check_result="${{ needs.workspace-check.result }}" package_check_result="${{ needs.package-check.result }}" test_result="${{ needs.test.result }}" + restricted_hermetic_result="${{ needs.restricted-hermetic.result }}" build_result="${{ needs.build.result }}" cross_platform_vm_result="${{ needs.cross-platform-vm.result }}" linux_distro_container_result="${{ needs.linux-distro-container.result }}" @@ -567,6 +594,7 @@ jobs: echo "workspace-check=${workspace_check_result}" echo "package-check=${package_check_result}" echo "test=${test_result}" + echo "restricted-hermetic=${restricted_hermetic_result}" echo "build=${build_result}" echo "cross-platform-vm=${cross_platform_vm_result}" echo "linux-distro-container=${linux_distro_container_result}" @@ -576,8 +604,8 @@ jobs: check_pr_governance - if [ "$lint_result" != "success" ] || [ "$workspace_check_result" != "success" ] || [ "$package_check_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ] || [ "$cross_platform_vm_result" != "success" ] || [ "$linux_distro_container_result" != "success" ] || [ "$docker_smoke_result" != "success" ]; then - echo "Required CI jobs did not pass: lint=${lint_result} workspace-check=${workspace_check_result} package-check=${package_check_result} test=${test_result} build=${build_result} cross-platform-vm=${cross_platform_vm_result} linux-distro-container=${linux_distro_container_result} docker-smoke=${docker_smoke_result}" + if [ "$lint_result" != "success" ] || [ "$workspace_check_result" != "success" ] || [ "$package_check_result" != "success" ] || [ "$test_result" != "success" ] || [ "$restricted_hermetic_result" != "success" ] || [ "$build_result" != "success" ] || [ "$cross_platform_vm_result" != "success" ] || [ "$linux_distro_container_result" != "success" ] || [ "$docker_smoke_result" != "success" ]; then + echo "Required CI jobs did not pass: lint=${lint_result} workspace-check=${workspace_check_result} package-check=${package_check_result} test=${test_result} restricted-hermetic=${restricted_hermetic_result} build=${build_result} cross-platform-vm=${cross_platform_vm_result} linux-distro-container=${linux_distro_container_result} docker-smoke=${docker_smoke_result}" exit 1 fi diff --git a/docs/ci-map.md b/docs/ci-map.md index 7c26c4482..5edd7c76f 100644 --- a/docs/ci-map.md +++ b/docs/ci-map.md @@ -12,7 +12,8 @@ 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: for Rust-impacting PRs and pushes, `CI Required Gate` requires `lint` + `test` + `restricted-hermetic` + `build` (no PR build-only bypass) + - Additional behavior: includes `Restricted Hermetic Validation` lane (`./scripts/ci/restricted_profile.sh`) that runs a capability-aware subset with isolated `HOME`/workspace/config roots and no external provider credentials - Additional behavior: rust-cache is partitioned per job role via `prefix-key` to reduce cache churn across lint/test/build/flake-probe lanes - Additional behavior: emits `test-flake-probe` artifact from single-retry probe when tests fail; optional blocking can be enabled with repository variable `CI_BLOCK_ON_FLAKE_SUSPECTED=true` - 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`) @@ -137,6 +138,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u - Keep required check naming stable and documented in `docs/operations/required-check-mapping.md` before changing branch protection settings. - Follow `docs/release-process.md` for verify-before-publish release cadence and tag discipline. - Keep merge-blocking rust quality policy aligned across `.github/workflows/ci-run.yml`, `dev/ci.sh`, and `.githooks/pre-push` (`./scripts/ci/rust_quality_gate.sh` + `./scripts/ci/rust_strict_delta_gate.sh`). +- Reproduce restricted/hermetic CI behavior locally with `./scripts/ci/restricted_profile.sh` before changing workspace/home-sensitive runtime code. - Use `./scripts/ci/rust_strict_delta_gate.sh` (or `./dev/ci.sh lint-delta`) as the incremental strict merge gate for changed Rust lines. - Run full strict lint audits regularly via `./scripts/ci/rust_quality_gate.sh --strict` (for example through `./dev/ci.sh lint-strict`) and track cleanup in focused PRs. - Keep docs markdown gating incremental via `./scripts/ci/docs_quality_gate.sh` (block changed-line issues, report baseline issues separately). diff --git a/scripts/ci/restricted_profile.sh b/scripts/ci/restricted_profile.sh new file mode 100755 index 000000000..2d76a9c3f --- /dev/null +++ b/scripts/ci/restricted_profile.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Restricted-profile CI lane: +# - isolates HOME/XDG paths into a throwaway directory +# - forces workspace/config roots away from developer machine defaults +# - runs capability-aware tests that should not require external network access + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +cd "${REPO_ROOT}" + +TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/zeroclaw-restricted-profile.XXXXXX")" +cleanup() { + rm -rf "${TMP_ROOT}" +} +trap cleanup EXIT + +RESTRICTED_HOME="${TMP_ROOT}/home" +RESTRICTED_WORKSPACE="${TMP_ROOT}/workspace-root" +mkdir -p "${RESTRICTED_HOME}" "${RESTRICTED_WORKSPACE}" +chmod 700 "${RESTRICTED_HOME}" "${RESTRICTED_WORKSPACE}" + +ORIGINAL_HOME="${HOME:-}" +if [ -z "${RUSTUP_HOME:-}" ] && [ -n "${ORIGINAL_HOME}" ]; then + export RUSTUP_HOME="${ORIGINAL_HOME}/.rustup" +fi +if [ -z "${CARGO_HOME:-}" ] && [ -n "${ORIGINAL_HOME}" ]; then + export CARGO_HOME="${ORIGINAL_HOME}/.cargo" +fi +if [ -n "${CARGO_HOME:-}" ] && [ -d "${CARGO_HOME}/bin" ]; then + case ":${PATH}:" in + *":${CARGO_HOME}/bin:"*) ;; + *) export PATH="${CARGO_HOME}/bin:${PATH}" ;; + esac +fi + +export HOME="${RESTRICTED_HOME}" +export USERPROFILE="${RESTRICTED_HOME}" +export XDG_CONFIG_HOME="${RESTRICTED_HOME}/.config" +export XDG_CACHE_HOME="${RESTRICTED_HOME}/.cache" +export XDG_DATA_HOME="${RESTRICTED_HOME}/.local/share" +export ZEROCLAW_WORKSPACE="${RESTRICTED_WORKSPACE}" +mkdir -p "${XDG_CONFIG_HOME}" "${XDG_CACHE_HOME}" "${XDG_DATA_HOME}" + +# Keep credential/network assumptions explicit for this lane. +unset GEMINI_OAUTH_CLIENT_ID GEMINI_OAUTH_CLIENT_SECRET OPENAI_API_KEY ANTHROPIC_API_KEY +unset HTTP_PROXY HTTPS_PROXY ALL_PROXY +export NO_PROXY="127.0.0.1,localhost" + +tests=( + "skills::tests::load_skills_with_config_reads_open_skills_dir_without_network" + "onboard::wizard::tests::run_models_refresh_uses_fresh_cache_without_network" + "onboard::wizard::tests::quick_setup_respects_zero_claw_workspace_env_layout" + "config::schema::tests::load_or_init_workspace_override_uses_workspace_root_for_config" +) + +echo "Running restricted-profile hermetic subset (${#tests[@]} tests)" +for test_name in "${tests[@]}"; do + echo "==> cargo test --locked --lib ${test_name}" + cargo test --locked --lib "${test_name}" +done + +echo "Restricted-profile hermetic subset completed successfully."