Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 47 additions & 16 deletions .github/workflows/ios-ship.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,23 @@ on:

permissions:
contents: read
checks: read

concurrency:
group: ios-ship-codecaps
cancel-in-progress: false

jobs:
ship:
name: TestFlight archive + upload
# The scheduled tick used to queue macos-latest just to decide "skip".
# That decision, and the wait for Swift CI, both run on ubuntu. The Mac
# runner starts only when this commit should ship and has compiled.
gate:
name: Ship gate
if: github.event.repository.fork == false
runs-on: macos-latest
runs-on: ubuntu-latest
timeout-minutes: 90
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
IOS_DIST_P12_BASE64: ${{ secrets.IOS_DIST_P12_BASE64 }}
IOS_DIST_P12_PASSWORD: ${{ secrets.IOS_DIST_P12_PASSWORD }}
outputs:
should_ship: ${{ steps.gate.outputs.should_ship }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand All @@ -74,8 +73,45 @@ jobs:
--repo-root "$GITHUB_WORKSPACE" \
codecaps

- name: Assert stable Xcode (never beta)
- name: Wait for Swift CI compile checks on this commit
if: steps.gate.outputs.should_ship == '1'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
run: bash scripts/await-swift-ci-checks.sh

ship:
name: TestFlight archive + upload
needs: gate
if: >-
github.event.repository.fork == false &&
needs.gate.result == 'success' &&
needs.gate.outputs.should_ship == '1'
runs-on: macos-latest
timeout-minutes: 90
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
IOS_DIST_P12_BASE64: ${{ secrets.IOS_DIST_P12_BASE64 }}
IOS_DIST_P12_PASSWORD: ${{ secrets.IOS_DIST_P12_PASSWORD }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- name: Restore ship-state cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ios-fleet
key: ios-fleet-state-codecaps-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
ios-fleet-state-codecaps-${{ github.run_id }}-
ios-fleet-state-codecaps-

- name: Assert stable Xcode (never beta)
run: |
echo "DEVELOPER_DIR=${DEVELOPER_DIR}"
xcodebuild -version
Expand All @@ -89,7 +125,6 @@ jobs:
esac

- name: Install xcodegen
if: steps.gate.outputs.should_ship == '1'
run: |
if command -v xcodegen >/dev/null 2>&1; then
xcodegen --version
Expand All @@ -98,7 +133,6 @@ jobs:
fi

- name: Load Sentry ship secrets (Infisical, then repository secrets)
if: steps.gate.outputs.should_ship == '1'
uses: ./.github/actions/infisical-secrets
with:
names: SENTRY_DSN SENTRY_AUTH_TOKEN
Expand All @@ -110,7 +144,6 @@ jobs:
GH_FALLBACK_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

- name: Import signing + ASC key
if: steps.gate.outputs.should_ship == '1'
run: |
if [ -z "${ASC_KEY_ID:-}" ]; then
echo "::error::ASC_KEY_ID empty. Confirm ASC_/IOS_DIST_ secrets are configured."
Expand All @@ -119,11 +152,9 @@ jobs:
bash "$GITHUB_WORKSPACE/scripts/ios-appstore-gm-prepare.sh"

- name: Install App Store profiles
if: steps.gate.outputs.should_ship == '1'
run: bash "$GITHUB_WORKSPACE/scripts/ios-install-appstore-profiles.sh"

- name: Ship CodeCaps to TestFlight
if: steps.gate.outputs.should_ship == '1'
env:
IOS_TF_RELEASE_NOTES: "1"
IOS_MANUAL_SIGN: "1"
Expand Down
59 changes: 8 additions & 51 deletions .github/workflows/mac-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,73 +55,30 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# Every non-fork event waits here, on ubuntu, before a macos-latest
# release runner starts. Pull request checks are on the head SHA.
await-swift-ci:
if: github.event.repository.fork == false && github.event_name == 'push'
if: github.event.repository.fork == false
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
checks: read
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- 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
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: bash scripts/await-swift-ci-checks.sh

release:
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')
needs.await-swift-ci.result == 'success'
runs-on: macos-latest
timeout-minutes: 60
env:
Expand Down
22 changes: 21 additions & 1 deletion .github/workflows/macos-ship.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,35 @@ on:

permissions:
contents: read
checks: read

concurrency:
group: macos-ship-codecaps
cancel-in-progress: true

jobs:
await-swift-ci:
name: Wait for Swift CI
if: github.event.repository.fork == false && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Wait for Swift CI compile checks on this commit
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
run: bash scripts/await-swift-ci-checks.sh

ship:
name: Mac TestFlight archive + upload
if: github.event.repository.fork == false && github.ref == 'refs/heads/main'
needs: await-swift-ci
if: >-
github.event.repository.fork == false &&
github.ref == 'refs/heads/main' &&
needs.await-swift-ci.result == 'success'
runs-on: macos-latest
timeout-minutes: 90
env:
Expand Down
31 changes: 21 additions & 10 deletions docs/rollouts/2026-10-15-cc19-compile-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,28 @@ document gives Jay the exact check names to add in GitHub (admin-only).
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).
- **Mac Release (Sparkle)** — `await-swift-ci` on `ubuntu-latest` polls until
`Mac package build and test` and `iOS companion compile` succeed on the
commit (or fails closed if either fails, is skipped, is cancelled, or
times out). Push, pull request, and `workflow_dispatch` all wait, so the
bundle build does not take a macOS runner beside Swift CI. The release job
`needs: await-swift-ci`, so Sparkle cannot publish while companion is
still running.
- **iOS and Mac TestFlight** — the same wait runs before a ship. The
scheduled iOS gate itself runs on ubuntu, so a skip no longer queues a
macOS runner.
- **Ruleset script** — `scripts/apply-main-compile-gate-ruleset.sh` prints
the rule change and refuses `--apply` until the three contexts below are
already success on the default branch. It is not run from CI.

## Verification (agents)

```bash
python3 scripts/compile_gate_checks_test.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules medium

Verification section gap in docs/rollouts/2026-10-15-cc19-compile-gate.md: rule 14 requires the section to list, in order, npm run lint, npx tsc --noEmit, npm test, and npm run build, each paired with a pass/fail outcome, but the added line is a bare python3 scripts/compile_gate_checks_test.py that names none of the four required CI guard commands and carries no pass/fail status, so the note reads as ad-hoc verification rather than the mandated four-step build/test gate.

Also found in:

  • docs/rollouts/2026-10-15-cc19-compile-gate.md:40-40
  • docs/rollouts/2026-10-15-cc19-compile-gate.md:39-39

Kody rule violation: Mandatory pre-commit handoff protocol and verification trio before claiming work done

Prompt for LLM

File docs/rollouts/2026-10-15-cc19-compile-gate.md:

Line 36:

Verification section gap in `docs/rollouts/2026-10-15-cc19-compile-gate.md`: rule 14 requires the section to list, in order, `npm run lint`, `npx tsc --noEmit`, `npm test`, and `npm run build`, each paired with a pass/fail outcome, but the added line is a bare `python3 scripts/compile_gate_checks_test.py` that names none of the four required CI guard commands and carries no pass/fail status, so the note reads as ad-hoc verification rather than the mandated four-step build/test gate.

**Also found in:**
- `docs/rollouts/2026-10-15-cc19-compile-gate.md:40-40`
- `docs/rollouts/2026-10-15-cc19-compile-gate.md:39-39`

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

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'))"
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ios-ship.yml'))"
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/macos-ship.yml'))"
```

Hosted macOS compile is validated on the pull request’s Swift CI run after push.
Expand Down Expand Up @@ -71,7 +80,9 @@ Do **not** require workflow names (`Swift CI`, `Secret scanning`). Require the

## Deferred

- Ruleset edit itself (Jay admin).
- Applying the ruleset. After a green Swift CI on `main`, someone with
ruleset admin runs `scripts/apply-main-compile-gate-ruleset.sh --apply`.
A dry run is the same command without `--apply`.
- Moving compile to self-hosted macOS runners (fleet BotFleet pinning work).
- Requiring Mac Release or TestFlight workflows as merge gates (ship lanes, not
compile).
- Requiring the ship workflows themselves as merge checks. They wait on the
compile checks; they are not compile checks.
89 changes: 89 additions & 0 deletions scripts/apply-main-compile-gate-ruleset.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Add required status checks to CodeCaps ruleset 24433457.
#
# Dry-run unless --apply. --apply refuses unless the Swift CI job names
# and gitleaks are already success on the default branch tip, so a starved or
# red Swift CI cannot become a gate that wedges every merge.
#
# Needs a token that can administer repository rulesets (GITHUB_TOKEN from
# Actions cannot). The token is never printed.
#
# scripts/apply-main-compile-gate-ruleset.sh
# scripts/apply-main-compile-gate-ruleset.sh --apply

set -euo pipefail

ROOT="$(cd "$(dirname "$0")" && pwd)"
REPO="${REPO:-Simple-With-Us/CodeCaps}"
RULESET_ID="${RULESET_ID:-24433457}"
APPLY=0

if [[ "${1:-}" == "--apply" ]]; then
APPLY=1
elif [[ -n "${1:-}" ]]; then
echo "usage: $0 [--apply]" >&2
exit 2
fi

[[ "$REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || {
echo "REPO is not owner/name" >&2
exit 1
}

current="$(mktemp)"
updated="$(mktemp)"
trap 'rm -f "$current" "$updated"' EXIT

gh api "repos/${REPO}/rulesets/${RULESET_ID}" >"$current"
python3 "$ROOT/compile_gate_ruleset.py" --ruleset "$current" >"$updated"

echo "ruleset ${RULESET_ID} on ${REPO}"
python3 - "$current" "$updated" "$ROOT/compile_gate_ruleset.py" <<'PY'
import importlib.util
import json
import sys
from pathlib import Path

spec = importlib.util.spec_from_file_location("compile_gate_ruleset", sys.argv[3])
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
current = json.loads(Path(sys.argv[1]).read_text())
updated = json.loads(Path(sys.argv[2]).read_text())
print("current contexts:", ", ".join(module.contexts_of(current)) or "(none)")
print("updated contexts:", ", ".join(module.contexts_of(updated)))
print("other rules kept:", ", ".join(
rule.get("type") for rule in updated["rules"] if rule.get("type") != "required_status_checks"
))
PY

branch="$(gh api "repos/${REPO}" --jq .default_branch)"
sha="$(gh api "repos/${REPO}/commits/${branch}" --jq .sha)"
echo "default branch ${branch} @ ${sha}"

set +e
ONCE=1 REPO="$REPO" SHA="$sha" \
bash "$ROOT/await-swift-ci-checks.sh" \
"Mac package build and test" \
"iOS companion compile" \
"gitleaks"
checks=$?
set -e
if [[ "$checks" -eq 0 ]]; then
echo "compile checks on ${branch} are success"
else
echo "compile checks on ${branch} are not success (exit ${checks})"
fi

if [[ "$APPLY" -eq 0 ]]; then
echo "dry-run only. Re-run with --apply after those checks are success."
exit 0
fi

if [[ "$checks" -ne 0 ]]; then
echo "refusing --apply until Mac package build and test, iOS companion compile, and gitleaks are success on ${branch}" >&2
exit 1
fi

gh api --method PUT "repos/${REPO}/rulesets/${RULESET_ID}" --input "$updated" \
--jq '{id,name,enforcement,rule_types:[.rules[].type]}'
echo "updated ruleset ${RULESET_ID}"
Loading
Loading