Skip to content

Phase 4: Sandbox — All Three Platforms (macOS/Linux/Windows) #32

Phase 4: Sandbox — All Three Platforms (macOS/Linux/Windows)

Phase 4: Sandbox — All Three Platforms (macOS/Linux/Windows) #32

Workflow file for this run

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)"