diff --git a/.github/workflows/mac-release.yml b/.github/workflows/mac-release.yml index fd6c8757..12d4c05c 100644 --- a/.github/workflows/mac-release.yml +++ b/.github/workflows/mac-release.yml @@ -55,8 +55,73 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + await-swift-ci: + if: github.event.repository.fork == false && github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + checks: read + contents: read + steps: + - name: Wait for Swift CI compile checks on this commit + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + required_checks=("Mac package build and test" "iOS companion compile") + deadline=$(( $(date +%s) + 5100 )) + while [ "$(date +%s)" -lt "$deadline" ]; do + payload="$(gh api "repos/${REPO}/commits/${SHA}/check-runs?per_page=100")" + pending=0 + failed=0 + for check_name in "${required_checks[@]}"; do + conclusion="$(printf '%s' "$payload" | python3 -c ' + import json, sys + name = sys.argv[1] + data = json.load(sys.stdin) + matches = [r for r in data.get("check_runs", []) if r.get("name") == name] + if not matches: + print("missing") + else: + print(matches[0].get("conclusion") or "pending") + ' "$check_name")" + case "$conclusion" in + success) ;; + failure|cancelled|timed_out|action_required|stale) + echo "::error::Swift CI check '${check_name}' concluded ${conclusion} on ${SHA}" + failed=1 + ;; + missing|pending|""|null) + pending=1 + ;; + *) + pending=1 + ;; + esac + done + if [ "$failed" -eq 1 ]; then + exit 1 + fi + if [ "$pending" -eq 0 ]; then + echo "Swift CI compile checks succeeded on ${SHA}" + exit 0 + fi + echo "Waiting for Swift CI on ${SHA}..." + sleep 30 + done + echo "::error::Timed out waiting for Swift CI compile checks on ${SHA}" + exit 1 + release: - if: github.event.repository.fork == false + needs: await-swift-ci + # always() is required so release still runs when await-swift-ci is skipped + # (pull_request / workflow_dispatch); without it GitHub treats skipped needs as failure. + if: >- + always() && + github.event.repository.fork == false && + (needs.await-swift-ci.result == 'success' || needs.await-swift-ci.result == 'skipped') runs-on: macos-latest timeout-minutes: 60 env: diff --git a/.github/workflows/swift-ci.yml b/.github/workflows/swift-ci.yml index ce3e08aa..9b9ce928 100644 --- a/.github/workflows/swift-ci.yml +++ b/.github/workflows/swift-ci.yml @@ -1,18 +1,23 @@ name: Swift CI -# Validates the SwiftPM Mac package (Sources/Tests at repo root). -# The iOS companion job's SCHEME is a best guess: the checked-in -# CodeCapsCompanion.xcodeproj has no shared schemes -# (no xcshareddata/xcschemes), so verify the scheme name on first run. +# Hosted macOS compile gate for CodeCaps (CC-19). Validates the SwiftPM Mac +# package at the repo root and the iOS companion Xcode project. Job names +# below are the GitHub status contexts Jay should require on ruleset 24433457; +# see docs/rollouts/2026-10-15-cc19-compile-gate.md. +# +# The companion SCHEME is a best guess: the checked-in CodeCapsCompanion.xcodeproj +# has no shared schemes (no xcshareddata/xcschemes), so verify the scheme name +# on first run. on: push: branches: [main] pull_request: + workflow_dispatch: concurrency: group: swift-ci-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read @@ -23,7 +28,9 @@ env: jobs: package: + name: Mac package build and test runs-on: macos-latest + timeout-minutes: 45 steps: - uses: actions/checkout@v7 - name: Swift build @@ -41,7 +48,10 @@ jobs: if-no-files-found: warn companion: + name: iOS companion compile + needs: package runs-on: macos-latest + timeout-minutes: 45 steps: - uses: actions/checkout@v7 - name: Install XcodeGen and regenerate companion project diff --git a/AGENTS.md b/AGENTS.md index 7a65962d..44423866 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,6 +91,10 @@ swift build swift test ``` +Hosted compile gate: `.github/workflows/swift-ci.yml` and +`docs/rollouts/2026-10-15-cc19-compile-gate.md` (required check names for +ruleset `default-main-protection`). + The `script/build_and_run.sh` script is the canonical release pipeline — it bundles, codesigns, notarizes, staples, builds a DMG, and writes a ZIP + sha. Run it once to read the contract; it is also the only path that knows the diff --git a/docs/rollouts/2026-10-15-cc19-compile-gate.md b/docs/rollouts/2026-10-15-cc19-compile-gate.md new file mode 100644 index 00000000..068cf911 --- /dev/null +++ b/docs/rollouts/2026-10-15-cc19-compile-gate.md @@ -0,0 +1,77 @@ +# 2026-10-15 — CC-19: Swift compile gate (hosted macOS CI + ruleset handoff) + +## Summary + +CodeCaps had hosted Swift CI (`.github/workflows/swift-ci.yml`) but ruleset +`default-main-protection` (id `24433457`) did not require any status checks, +so merges and Sparkle releases could proceed while compile jobs were still +queued. CC-19 wires the lane for a real gate: one macOS runner per pull +request, Mac Release waits for the same compile checks on `main`, and this +document gives Jay the exact check names to add in GitHub (admin-only). + +## What changed + +- **Swift CI** — stay on `macos-latest` (Swift 6 toolchain required by + `sentry-cocoa` 9.30.x), cancel stale pull-request runs, run `companion` + only after `package` (`needs:`) so a PR never holds two macOS runners at + once. Explicit job `name:` values match the contexts GitHub reports for + required checks. +- **Mac Release (Sparkle)** — new `await-swift-ci` job on `ubuntu-latest` + polls until `package` and `companion` succeed on the merge commit (or fails + closed if either fails, is cancelled, or times out). The release job + `needs: await-swift-ci`, closing the race where Sparkle published while + companion was still running. +- **This rollout** — owner steps for ruleset required checks (not applied in + CI; Jay has the admin grant). + +## Verification (agents) + +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/swift-ci.yml'))" +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/mac-release.yml'))" +``` + +Hosted macOS compile is validated on the pull request’s Swift CI run after push. + +## Required status checks (Jay — after this is on `main`) + +Ruleset: **Settings → Rules → Rulesets → `default-main-protection`** +(`24433457`) → add a **Require status checks to pass** rule. + +Require these **job** contexts (not the workflow title `Swift CI`): + +| Context | Workflow | What it proves | +| --- | --- | --- | +| `Mac package build and test` | Swift CI | `swift build`, `swift test`, native UI fixture render | +| `iOS companion compile` | Swift CI | Companion XcodeGen build, simulator smoke, Mac widget target | +| `gitleaks` | Secret scanning | Secret scan on the PR commit range | + +**Order of operations:** land this PR and confirm at least one green Swift CI +on `main` **before** ticking the two Swift contexts required. If macOS hosted +runners are backlogged, a required check that has not started yet blocks every +pull request — fix queue health first, then enable the Swift contexts. + +Optional strictness: leave **Require branches to be up to date before merging** +off unless you want merge-queue semantics; Swift CI already runs on the head +commit. + +Confirm live contexts: + +```bash +gh api repos/Simple-With-Us/CodeCaps/rulesets/24433457 \ + --jq '.rules[] | select(.type=="required_status_checks")' +``` + +After you add the rule, the same command should list the three contexts above +under `parameters.required_status_checks` (or `contexts`, depending on API +shape). + +Do **not** require workflow names (`Swift CI`, `Secret scanning`). Require the +**job** names exactly as in the table. + +## Deferred + +- Ruleset edit itself (Jay admin). +- Moving compile to self-hosted macOS runners (fleet BotFleet pinning work). +- Requiring Mac Release or TestFlight workflows as merge gates (ship lanes, not + compile).