Phase 4: Sandbox — All Three Platforms (macOS/Linux/Windows) #32
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: | |
| - 'v*.*.*' | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Least-privilege defaults. Jobs that need more elevate explicitly. | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| dco: | |
| name: DCO signoff check | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| # Inline DCO check. tim-actions/dco crashed with "Argument list too long" | |
| # for PRs >~20 commits because it passes the full commit JSON array as a | |
| # single process argument. This shell version scans each commit on the PR | |
| # branch via `git log`, which scales to arbitrary PR size and produces | |
| # clear per-commit error messages. Also Node-deprecation-proof. | |
| - name: Verify Signed-off-by on every PR commit | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -eu | |
| bad=0 | |
| count=0 | |
| for commit in $(git rev-list "$BASE_SHA..$HEAD_SHA"); do | |
| count=$((count + 1)) | |
| author=$(git log --format='%an <%ae>' -n 1 "$commit") | |
| msg=$(git log --format=%B -n 1 "$commit") | |
| if ! printf '%s\n' "$msg" | grep -qE '^Signed-off-by: .+ <.+@.+>\s*$'; then | |
| echo "::error::Commit $commit by $author is missing a Signed-off-by trailer." | |
| bad=1 | |
| fi | |
| done | |
| if [ "$bad" -eq 1 ]; then | |
| echo "DCO check FAILED. Amend the offending commits with 'git commit --amend -s' (or rebase with 'git rebase --signoff') and force-push." | |
| exit 1 | |
| fi | |
| echo "DCO check passed: $count commit(s) all carry Signed-off-by trailers." | |
| lint: | |
| name: Lint (fmt + clippy + deny + audit) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Detect Cargo workspace | |
| id: detect | |
| run: | | |
| if [ -f Cargo.toml ]; then | |
| echo "has_cargo=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_cargo=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Pin to the same version as rust-toolchain.toml to avoid downloading two | |
| # toolchains per CI run (stable + the pinned channel) and to guarantee the | |
| # tri-OS matrix tests the exact version developers use locally. | |
| - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95 | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| with: | |
| components: rustfmt, clippy | |
| - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| # protoc is required by forge_repo (prost-build → tonic-prost-build) to | |
| # compile .proto files for google-cloud-auth. Clippy compiles build | |
| # scripts, so lint needs protoc just like the test job. | |
| - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| with: | |
| version: "27.x" | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| # Phase 2.5 split kay-core into 23 forge_* sub-crates; the workspace now | |
| # builds cleanly without exclusions. | |
| - run: cargo fmt --all -- --check | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| - run: cargo clippy --workspace --all-targets --all-features -- -D warnings | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| - uses: EmbarkStudios/cargo-deny-action@5bb39ff5d5a0e94dc9e2dc94eced0c6129743a57 # v2 | |
| if: steps.detect.outputs.has_cargo == 'true' && hashFiles('deny.toml') != '' | |
| - name: cargo-audit | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| run: | | |
| cargo install cargo-audit --locked --quiet | |
| cargo audit | |
| test: | |
| name: Test (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-14, windows-latest] | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Detect Cargo workspace | |
| id: detect | |
| shell: bash | |
| run: | | |
| if [ -f Cargo.toml ]; then | |
| echo "has_cargo=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_cargo=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95 | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| # protoc is required by forge_repo (prost-build → tonic-prost-build) and | |
| # anything else downstream that compiles .proto files at build time. | |
| # dtolnay/rust-toolchain does not install it; CI images ship without it. | |
| - uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0 | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| with: | |
| version: "27.x" | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| # Test scope is limited to Kay-owned crates (kay-*). The imported | |
| # forge_* sub-crates (Phase 2.5 split) include upstream tests that | |
| # require orchestration/runtime infrastructure not yet wired up here — | |
| # that integration lands in Phase 3 (tool registry + agent loop). | |
| # Compilation is still validated across the full workspace via the | |
| # Lint job's `cargo clippy --workspace --all-targets`; this job only | |
| # asserts Kay-authored test suites pass. | |
| - run: cargo test --all-features | |
| -p kay-cli -p kay-core -p kay-provider-openrouter | |
| -p kay-sandbox-linux -p kay-sandbox-macos -p kay-sandbox-policy | |
| -p kay-sandbox-windows -p kay-tools | |
| -p kay-tauri -p kay-tui | |
| if: steps.detect.outputs.has_cargo == 'true' | |
| frontend: | |
| name: Frontend (ui) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Detect frontend | |
| id: detect | |
| run: | | |
| if [ -f crates/kay-tauri/ui/package.json ]; then | |
| echo "has_ui=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_ui=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 | |
| if: steps.detect.outputs.has_ui == 'true' | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| if: steps.detect.outputs.has_ui == 'true' | |
| with: | |
| node-version: '20' | |
| cache: pnpm | |
| cache-dependency-path: crates/kay-tauri/ui/pnpm-lock.yaml | |
| - run: pnpm install --frozen-lockfile | |
| working-directory: crates/kay-tauri/ui | |
| if: steps.detect.outputs.has_ui == 'true' | |
| - run: pnpm run typecheck | |
| working-directory: crates/kay-tauri/ui | |
| if: steps.detect.outputs.has_ui == 'true' | |
| - run: pnpm run lint | |
| working-directory: crates/kay-tauri/ui | |
| if: steps.detect.outputs.has_ui == 'true' | |
| - run: pnpm run test -- --run | |
| working-directory: crates/kay-tauri/ui | |
| if: steps.detect.outputs.has_ui == 'true' | |
| - run: pnpm run build | |
| working-directory: crates/kay-tauri/ui | |
| if: steps.detect.outputs.has_ui == 'true' | |
| signed-tag-gate: | |
| name: Block unsigned tag on release | |
| runs-on: ubuntu-latest | |
| # Fires on any v*.*.* release tag EXCEPT the v0.0.x pre-stable series. | |
| # v0.0.x ships as internal/audit builds before Phase 11 signing-key | |
| # procurement completes — the signing requirement kicks in from v0.1.0. | |
| # Rationale: SECURITY.md §Release Signing, docs/CICD.md §Gates. | |
| if: | | |
| github.ref_type == 'tag' && | |
| startsWith(github.ref_name, 'v') && | |
| contains(github.ref_name, '.') && | |
| !startsWith(github.ref_name, 'v0.0.') | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| # actions/checkout@v4 defaults fetch-tags to false. Without it, the | |
| # tag that triggered this workflow is recreated locally as a | |
| # lightweight tag pointing at the commit, and `git tag -v` fails | |
| # with "cannot verify a non-tag object of type commit" — even when | |
| # the pushed tag is annotated and signed. We need the actual tag | |
| # object here to verify the signature. | |
| fetch-tags: true | |
| - name: Restore annotated tag object | |
| # actions/checkout@v4, even with fetch-tags: true, performs a second | |
| # fetch of the form `+<sha>:refs/tags/<TAG>` which OVERWRITES the | |
| # annotated tag we just fetched with a lightweight tag pointing at | |
| # the underlying commit. Force-refetch the annotated tag object so | |
| # `git tag -v` sees the signature. | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| git fetch origin --force "refs/tags/$TAG:refs/tags/$TAG" | |
| git cat-file -t "$TAG" # expect: tag | |
| - name: Configure SSH allowed_signers for tag verification | |
| # git tag -v with SSH signatures requires gpg.ssh.allowedSignersFile. | |
| # The repo-committed .github/allowed_signers is the single source of | |
| # truth for authorized release signers. Add new signers by appending | |
| # a line and getting the change merged through a normal PR. | |
| run: | | |
| git config gpg.ssh.allowedSignersFile "$GITHUB_WORKSPACE/.github/allowed_signers" | |
| - name: Verify tag signature | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| if ! git tag -v "$TAG"; then | |
| echo "::error::Tag $TAG is not GPG/SSH-signed by an authorized signer (see .github/allowed_signers and PROJECT.md GOV-05)." >&2 | |
| exit 1 | |
| fi | |
| parity-gate: | |
| name: Parity gate (EVAL-01) — scaffolded, not run in P1 | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Check for archived parity run | |
| run: | | |
| ARCHIVE=".planning/phases/01-fork-governance-infrastructure/parity-baseline" | |
| if [ ! -f "$ARCHIVE/summary.md" ]; then | |
| echo "::notice::Parity run not yet executed — EVAL-01a is the follow-on task that produces $ARCHIVE/summary.md." | |
| echo "Phase 1 ships scaffolding only per user amendment 2026-04-19." | |
| exit 0 | |
| fi | |
| SCORE=$(grep -oP 'score:\s*\K[0-9.]+' "$ARCHIVE/summary.md") | |
| echo "Archived parity score: $SCORE" | |
| python3 -c "exit(0 if float('$SCORE') >= 80.0 else 1)" |