Skip to content

Replace hosted CI with a local validation check - #586

Merged
bradleymackey merged 3 commits into
mainfrom
devops/self-hosted-runner
Sep 25, 2026
Merged

bradleymackey merged 3 commits into
mainfrom
devops/self-hosted-runner

Conversation

@bradleymackey

@bradleymackey bradleymackey commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Every change to Vault is authored and tested on the developer's Mac, so running CI on top of that is overhead. This PR removes the Validate workflow and replaces it with make validate, which validates the exact commit locally and posts the result to GitHub as the Validate (local) commit status. A new ruleset on main requires that status, with an admin bypass for emergencies, so a green check is still needed to merge.

  • scripts/validate.sh checks the commit out into a separate worktree with its own DerivedData, so uncommitted changes and your usual build folders can't affect it. With Xcode 27.0 it then runs:

    • make lint;
    • the Fastlane config check, which is skipped, and noted on the status, if the Ruby in .ruby-version isn't installed;
    • a build and full run of the iOSAllTests plan on a throwaway iPhone 18 Pro Max / iOS 27.0 simulator, created for the run and deleted afterwards.

    The simulator has to be named exactly "iPhone 18 Pro Max", because the snapshot tests check the device name. The script therefore addresses it by UDID. Test diagnostics collection is off, so a failing run reports straight away instead of hanging for up to 10 minutes. If the run is interrupted, the status is set to error.

  • .githooks/pre-push posts the stored result for any commit validated before it was pushed, so you can validate before or after pushing. It never blocks a push. Enable it once per clone with git config core.hooksPath .githooks.

  • Docs: README.md gains a Validation section, and Vault/README.md lists make validate. The AGENTS.md files tell agents to validate after every commit on a PR branch, and never to post the status by hand or work around a failing test.

  • Removed: .github/workflows/validate-all.yml.

The check is self-attested: it records that the commit passed on the machine that posted it, not on independent CI.

This PR started as moving Validate onto a self-hosted Mac runner, then onto ephemeral macOS VMs with the 14 test shards merged into one job. In the end, local validation replaced the workflow entirely, so the net diff removes it.

Test plan

  • make validate on this PR's head commit: lint, build and all tests passed in 3m27s, with no test-host restarts. The Fastlane check was skipped because Ruby 4.0.5 isn't installed on this Mac.
  • The pre-push hook posted Validate (local): success to the commit seconds after the push, and no Actions run was triggered.
  • A failing run is reported as a failure and stored as one: an earlier run, with the simulator misnamed, failed the snapshot tests' device check.
  • The main ruleset requires Validate (local), and this PR shows as mergeable with the check present.

🤖 Generated with Claude Code

bradleymackey and others added 3 commits September 25, 2026 11:39
No GitHub-hosted GA image has Xcode 27 / iOS 27.0 yet, so Validate had
been manual-only. Move Lint, CI_iOS Build and CI_iOS Tests onto the
badbundle self-hosted runner ([self-hosted, macOS, studio]) and turn the
pull_request and push triggers back on.

- Skip the self-hosted jobs for pull requests from forks, since the repo
  is public.
- Select Xcode with DEVELOPER_DIR instead of `sudo xcode-select`; the
  runner has no passwordless sudo.
- Keep Release Config on GitHub-hosted macos-26 (it only needs Ruby).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The self-hosted runner runs one job at a time, in a fresh macOS VM per job,
so the 14 per-suite test shards only added overhead: on the first run,
about 85% of each shard's time was repeated setup (checkout, downloading
and unpacking the test products, booting the simulator), and the tests
themselves were 308s of 2050s.

- Merge CI_iOS Build and the CI_iOS Tests matrix into one CI_iOS Build &
  Test job that builds for testing and then runs the whole iOSAllTests
  plan (the same 14 suites the matrix listed), with parallel testing still
  off. The CI_iOS check job keeps its name.
- Correct the header: the fork `if:` guard is not a security boundary,
  since a fork PR can edit this file; the approval requirement for
  external contributors is what protects the runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every change is authored and tested on the developer's Mac, so running CI
as well is overhead. Instead, `make validate` validates the exact commit
locally and posts the result to GitHub as the "Validate (local)" status,
which main will require before a PR can merge.

- scripts/validate.sh checks the commit out into a separate worktree with
  its own DerivedData, then runs lint, the Fastlane config check (skipped
  and noted if the pinned Ruby isn't installed), and a build and full run
  of the iOSAllTests plan on a dedicated, freshly erased iPhone 18 Pro Max
  / iOS 27.0 simulator with Xcode 27.0. Test diagnostics collection is off,
  so a failure doesn't hang for 10 minutes.
- .githooks/pre-push posts stored results for commits validated before
  they were pushed. It never blocks a push.
- Remove the Validate workflow, and document the process in README.md,
  Vault/README.md and the AGENTS.md files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bradleymackey bradleymackey changed the title Run the Xcode 27 validation jobs on the self-hosted Mac runner Replace hosted CI with a local validation check Sep 25, 2026
@bradleymackey
bradleymackey merged commit 6342779 into main Sep 25, 2026
1 check passed
@bradleymackey
bradleymackey deleted the devops/self-hosted-runner branch September 25, 2026 13:26
bradleymackey added a commit that referenced this pull request Sep 25, 2026
…r rbenv (#587)

## Summary

This fixes two leftovers from #586: a flaky test that could fail
validation, and the Fastlane check that validation was skipping.

- **Flaky test:** the "Cancellation is always checked before returning"
tests in `Task+RaceTests.swift` created a task and then cancelled it.
The task could run `Task.race` to completion before `cancel()` was
called. When it did, the race wasn't cancelled and correctly threw
`.noTasksScheduled` instead of `CancellationError`. Both `FirstValue`
and `FirstResolved` have this race. The tests now wait for the
cancellation, using the existing `TaskCancellationWaiter`, before
starting the race. `Task.race` itself is unchanged.
- **Fastlane check:** Ruby 4.0.5 from `.ruby-version` is now installed
on the validating Mac through rbenv. `validate.sh` now also puts rbenv's
shims on `PATH` itself. Before this, shells that hadn't run `rbenv init`
(non-interactive ones, like an agent's) found the system Ruby 2.6 and
still skipped the check.

## Testing

- **Before the fix:** running `FoundationExtensionsTests/TaskRaceTests`
on macOS with `-test-iterations 500 -run-tests-until-failure`,
`FirstResolved` failed with `.noTasksScheduled`.
- **After the fix:** 3,000 repetitions all passed.
- `bundle exec fastlane lanes` succeeds on Ruby 4.0.5, and
`Gemfile.lock` is unchanged.
- `make validate` passed on c34aa3c in 3m36s. This time the Fastlane
config step ran instead of being skipped.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant