diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 7c75a81..594bfbb 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -32,7 +32,9 @@ jobs: contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association)) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.review.author_association)) - runs-on: ubuntu-latest + # Claude has write permissions and reads ANTHROPIC_API_KEY, so keep it on a + # trusted private runner instead of the public shell-only fleet. + runs-on: ['self-hosted', 'private', 'macOS', 'ARM64', 'xcode'] timeout-minutes: 30 permissions: contents: write @@ -69,8 +71,9 @@ jobs: Use CLAUDE.md and docs/bootstrap/onboarding.md as repo policy context. Keep required PR status checks aligned with CI Gate. Preserve the split fast and extended validation model. - Shell-safe jobs may use `[self-hosted, synology, shell-only, public]`. - Docker, service-container, browser, and `container:` jobs stay on GitHub-hosted runners. + Shell-safe jobs must use `[self-hosted, linux, shell-only, public]`. + Secret-bearing automation must stay on a trusted private runner. + Docker, service-container, browser, and `container:` jobs require a dedicated self-hosted pool with matching capability labels. Prefer the smallest safe change and add tests for behavior changes. MANUAL TASK: ${{ github.event.inputs.prompt }} diff --git a/.github/workflows/extended-validation.yml b/.github/workflows/extended-validation.yml index 3bb391f..a5c2852 100644 --- a/.github/workflows/extended-validation.yml +++ b/.github/workflows/extended-validation.yml @@ -25,7 +25,7 @@ defaults: jobs: changes: name: Detect Extended Validation Scope - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] outputs: app: ${{ steps.preset.outputs.app || steps.filter.outputs.app || 'false' }} ci: ${{ steps.preset.outputs.ci || steps.filter.outputs.ci || 'false' }} @@ -77,7 +77,7 @@ jobs: fast-checks: name: Fast Checks - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] timeout-minutes: 15 needs: changes if: needs.changes.outputs.app == 'true' || needs.changes.outputs.ci == 'true' @@ -111,7 +111,7 @@ jobs: validate-secrets: name: Validate Secrets - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -120,7 +120,7 @@ jobs: extended-validation-gate: name: Extended Validation Gate - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] if: always() needs: - changes diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 39cbcbb..79ecd6d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,8 +5,9 @@ on: jobs: lint: - # Hosted fallback: the Synology shell-only pool does not provide a C toolchain, - # and apt-based provisioning is blocked by container permissions. + # Hosted fallback: the self-hosted shell-only Linux pool does not provide a + # C toolchain, and the Docker-capable pool currently has an incompatible + # Docker client for Actions container jobs. runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr-fast-ci.yml b/.github/workflows/pr-fast-ci.yml index 749539c..c835e11 100644 --- a/.github/workflows/pr-fast-ci.yml +++ b/.github/workflows/pr-fast-ci.yml @@ -23,7 +23,7 @@ defaults: jobs: changes: name: Detect Relevant Changes - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] outputs: app: ${{ steps.filter.outputs.app }} ci: ${{ steps.filter.outputs.ci }} @@ -58,7 +58,7 @@ jobs: fast-checks: name: Fast Checks - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] timeout-minutes: 15 needs: changes if: >- @@ -100,7 +100,7 @@ jobs: validate-secrets: name: Validate Secrets - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] timeout-minutes: 10 if: github.event.pull_request.draft == false steps: @@ -112,7 +112,7 @@ jobs: ci-gate: name: CI Gate - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] if: always() needs: - changes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 112000d..d16297f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -244,7 +244,7 @@ jobs: name: Publish Homebrew tap PR needs: release if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest + runs-on: ['self-hosted', 'linux', 'shell-only', 'public'] continue-on-error: true env: TAP_REPOSITORY: ${{ vars.HOMEBREW_TAP_REPOSITORY || 'OMT-Global/homebrew-apw' }} diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 025b354..6637a8d 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -7,8 +7,9 @@ permissions: jobs: test: - # Hosted fallback: the Synology shell-only pool does not provide a C toolchain, - # and apt-based provisioning is blocked by container permissions. + # Hosted fallback: the self-hosted shell-only Linux pool does not provide a + # C toolchain, and the Docker-capable pool currently has an incompatible + # Docker client for Actions container jobs. runs-on: ubuntu-latest steps: diff --git a/AGENTS.md b/AGENTS.md index 8e0530a..e1d5fd9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ - Always work on a feature branch. Hooks block commits to `main` and `master`; enable them with `git config core.hooksPath .githooks`. - Stack baseline: Generic polyglot. - CI baseline: fast PR checks stay cheap and shell-safe; extended validation runs on `main`, nightly, or manual dispatch. -- Self-hosted runner policy: shell-safe jobs may use `[self-hosted, synology, shell-only, public]`; anything needing Docker, service containers, browser infra, or `container:` must stay on GitHub-hosted runners. +- Self-hosted runner policy: shell-safe jobs must use `[self-hosted, linux, shell-only, public]`; anything needing Docker, service containers, browser infra, or `container:` must use a dedicated self-hosted runner pool with the matching capability labels. - Add or update tests for every interactive, branching, or operator-facing behavior change. - Never commit real secrets, runtime auth, or machine-local env files. Use templates and GitHub environments instead. diff --git a/docs/bootstrap/onboarding.md b/docs/bootstrap/onboarding.md index 74594d9..9fd87b7 100644 --- a/docs/bootstrap/onboarding.md +++ b/docs/bootstrap/onboarding.md @@ -37,10 +37,10 @@ ## Runner Policy - Run `apw --json doctor` first on a new local checkout or self-hosted runner to confirm the Rust, Xcode, secret-scan, signing, and runner-environment diagnostics before extended validation. - - Shell-safe jobs may use `[self-hosted, synology, shell-only, public]`. - - Docker, service-container, browser, and `container:` workloads stay on GitHub-hosted runners. + - Shell-safe jobs must use `[self-hosted, linux, shell-only, public]`. + - Docker, service-container, browser, and `container:` workloads require a dedicated self-hosted runner pool with matching capability labels. - Keep PR checks cheap. Add heavy validation to `scripts/ci/run-extended-validation.sh` instead of the PR lane. - - APW extended validation requires both Rust (`cargo`) and the macOS Swift toolchain, so the `extended-checks` job must run on the org macOS self-hosted pool (`[self-hosted, private, macOS, ARM64, xcode]`) rather than the Synology shell-only pool. + - APW extended validation requires both Rust (`cargo`) and the macOS Swift toolchain, so the `extended-checks` job must run on the org macOS self-hosted pool (`[self-hosted, private, macOS, ARM64, xcode]`) rather than the Linux shell-only pool. - Rust builds OpenSSL through the `openssl` crate's vendored feature, so the macOS runner needs source-build tools (`cc`, `make`, and `perl`) but does not require Homebrew, pkg-config, or a system OpenSSL prefix. - Extended validation runs `scripts/ci/run-native-app-preflight.sh`, which exercises the Swift package through `xcodebuild`, builds `APW.app`, verifies codesign, and confirms the associated-domain entitlement is embedded. @@ -87,5 +87,5 @@ - First-party Claude web sessions should use `bash scripts/claude-cloud/setup.sh` in `claude.ai/code`. - Interactive Claude work is prepared through `.devcontainer/devcontainer.json`. -- GitHub-hosted Claude automation lives in `.github/workflows/claude.yml` and is intentionally separate from the required PR checks. +- Claude automation lives in `.github/workflows/claude.yml` on the shared self-hosted Linux pool and is intentionally separate from the required PR checks. - Finish GitHub-side auth by running `/install-github-app` in Claude Code or adding `ANTHROPIC_API_KEY` as a repo secret. diff --git a/rust/src/native_app.rs b/rust/src/native_app.rs index a50ca61..fac885c 100644 --- a/rust/src/native_app.rs +++ b/rust/src/native_app.rs @@ -492,7 +492,7 @@ fn ci_runner_environment_check() -> Value { "WARN", "runner_labels", "Not running in GitHub Actions; runner labels cannot be verified locally", - "In CI, confirm shell-safe jobs use [self-hosted, synology, shell-only, public] and extended macOS validation uses [self-hosted, private, macOS, ARM64, xcode].", + "In CI, confirm shell-safe jobs use [self-hosted, linux, shell-only, public] and extended macOS validation uses [self-hosted, private, macOS, ARM64, xcode].", ) } @@ -2006,12 +2006,31 @@ mod tests { result } + fn with_ci_env(value: Option<&str>, run: F) -> R + where + F: FnOnce() -> R, + { + let previous_value = env::var("CI").ok(); + if let Some(value) = value { + env::set_var("CI", value); + } else { + env::remove_var("CI"); + } + let result = run(); + if let Some(value) = previous_value { + env::set_var("CI", value); + } else { + env::remove_var("CI"); + } + result + } + #[test] #[serial] fn doctor_does_not_create_default_credentials_file_without_demo_gate() { with_temp_home(|| { with_demo_env(None, || { - let payload = native_app_doctor().unwrap(); + let payload = with_ci_env(None, || native_app_doctor().unwrap()); assert_eq!( payload["frameworks"]["authenticationServicesLinked"], json!(true) @@ -2029,6 +2048,19 @@ mod tests { "missing diagnostic {id}: {diagnostics:#?}" ); } + let runner_labels = diagnostics + .iter() + .find(|entry| entry["id"] == json!("runner_labels")) + .expect("runner labels diagnostic"); + let remediation = runner_labels["hint"].as_str().unwrap_or(""); + assert!( + remediation.contains("[self-hosted, linux, shell-only, public]"), + "runner remediation should document the Linux shell-only pool: {remediation}" + ); + assert!( + !remediation.contains("synology"), + "runner remediation should not mention the retired Synology pool: {remediation}" + ); assert!(diagnostics.iter().all(|entry| entry["status"] .as_str() .map(|status| ["OK", "WARN", "FAIL"].contains(&status))