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
33 changes: 7 additions & 26 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,27 +1,8 @@
#!/bin/bash
# Posts the "Validate (local)" status for each pushed branch commit that
# scripts/validate.sh has already validated (see README.md#validation). It never
# blocks the push: a commit that hasn't been validated just doesn't get the
# check, and main won't accept it until it does.
#
# Enable once per clone: git config core.hooksPath .githooks

repo_root=$(git rev-parse --show-toplevel)
state_dir="$(cd "$repo_root" && cd "$(git rev-parse --git-common-dir)" && pwd)/validate"
mkdir -p "$state_dir/logs"

while read -r _local_ref local_sha remote_ref _remote_sha; do
case "$remote_ref" in refs/heads/*) ;; *) continue ;; esac
case "$local_sha" in *[!0]*) ;; *) continue ;; esac # deleting the branch

if [ -f "$state_dir/results/$local_sha" ]; then
# The commit only reaches GitHub after this hook returns, so post from the
# background once it's there.
nohup "$repo_root/scripts/validate.sh" --post "$local_sha" \
</dev/null >>"$state_dir/logs/post.log" 2>&1 &
else
echo "validate: ${local_sha:0:9} (${remote_ref#refs/heads/}) hasn't been validated; run 'make validate' in Vault/ to get the green check." >&2
fi
done

#!/bin/sh
# Posts local-check results for pushed commits: https://github.com/badbundle/local-check
bin="$(git rev-parse --show-toplevel)/node_modules/.bin/local-check"
if [ -x "$bin" ]; then
exec "$bin" hook pre-push "$@"
fi
echo "local-check isn't installed, so results won't be posted. Run bun install." >&2
exit 0
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ vendor/
*.p12
*.mobileprovision
*.ipa

# Bun
node_modules/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Read [`MANIFESTO.md`](./MANIFESTO.md) before proposing or implementing any featu

## Validation

There is no hosted CI. Before a PR can merge into `main`, its latest commit needs the **Validate (local)** status check, which is posted by running `make validate` in `Vault/`. See [Validation](./README.md#validation) in the README and the rules in [`Vault/AGENTS.md`](./Vault/AGENTS.md).
There is no hosted CI. Before a PR can merge into `main`, its latest commit needs the **Validate (local)** status check, which is posted by running `make validate` in `Vault/`, using the checks in [`local-check.config.ts`](./local-check.config.ts). See [Validation](./README.md#validation) in the README and the rules in [`Vault/AGENTS.md`](./Vault/AGENTS.md).

## Layout

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ As soon as we are able, we will be dropping the xcodeproj project wrapper and go

There is no hosted CI. Changes are validated on the developer's Mac, and the result is posted to the commit on GitHub as the **Validate (local)** status check. `main` requires that check, so a PR can't be merged until its latest commit has passed.

1. Once per clone, enable the pre-push hook: `git config core.hooksPath .githooks`
1. Install [Bun](https://bun.com), then run `bun install` at the root of the repo, once per clone. It installs [local-check](https://github.com/badbundle/local-check), the tool that does the validating, and enables its pre-push hook.
2. Commit your changes, then run `make validate` from `/Vault`.

`make validate` ([`scripts/validate.sh`](./scripts/validate.sh)) checks out the exact commit into a separate worktree, so uncommitted changes and your usual DerivedData can't affect the result. It then runs, with Xcode 27.0:
The checks are in [`local-check.config.ts`](./local-check.config.ts). local-check checks out the exact commit into a separate worktree, so uncommitted changes and your usual DerivedData can't affect the result. With Xcode 27.0, it then runs:

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

If the commit is already on GitHub, the result is posted straight away. Otherwise it's stored, and the pre-push hook posts it when you push, so you can validate before or after pushing. Every new commit needs validating again. Logs are kept in `.git/validate/logs/`.
If the commit is already on GitHub, the result is posted straight away. Otherwise it's stored, and the pre-push hook posts it when you push, so you can validate before or after pushing. Every new commit needs validating again. Logs are kept in `.git/local-check/logs/`.

The check is self-attested: it records that the commit passed on the machine that posted it, rather than on independent CI.
5 changes: 3 additions & 2 deletions Vault/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ Before every commit, run `make format` and `make lint` from the `Vault/` directo

## Validating a Pull Request

There is no hosted CI. `main` only accepts a PR whose latest commit has the **Validate (local)** status check, and only `make validate` posts it (see [Validation](../README.md#validation)).
There is no hosted CI. `main` only accepts a PR whose latest commit has the **Validate (local)** status check, and only `make validate` posts it (see [Validation](../README.md#validation)). The checks it runs are in [`local-check.config.ts`](../local-check.config.ts).

- If `bun install` hasn't been run in this clone, run it at the root of the repo first.
- Commit first, then run `make validate` from the `Vault/` directory. It validates the committed `HEAD` in a clean worktree, so uncommitted changes aren't covered.
- Run it again after every new commit on a PR branch: each commit needs its own check.
- If it fails, fix the problem, commit, and validate the new commit. Never post, edit or fake the status by hand (for example with `gh api .../statuses`), and don't work around a failing test to get a green check.
- It takes several minutes. Tell the user whether it passed, and if it didn't, which step failed and where its log is.
- It takes several minutes. Tell the user whether it passed, and if it didn't, which check failed and where its log is.
8 changes: 5 additions & 3 deletions Vault/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ lint:
swift package plugin --allow-writing-to-package-directory swiftlint --strict --quiet ./Sources
swift package --allow-writing-to-package-directory format --lint --sources=./

# Validation: lint, build and test the current commit in a clean worktree, then
# post the green "Validate (local)" check to GitHub. See ../README.md#validation.
# Validation: runs the checks in ../local-check.config.ts on the current commit,
# in a clean worktree, then posts the green "Validate (local)" check to GitHub.
# Installing first keeps local-check at the version package.json pins.
# See ../README.md#validation.
.PHONY: validate
validate:
../scripts/validate.sh
cd .. && bun install --frozen-lockfile --silent && bun run --silent validate

# Marketing screenshots: capture raw shots of the app on throwaway simulators,
# then put them in device frames with titles. See Screenshots/README.md.
Expand Down
14 changes: 14 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions local-check.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ConfigFunction, Context } from "@badbundle/local-check";

// The checks `make validate` runs on a commit before it posts the green
// "Validate (local)" check. See README.md#validation.
export default (({ xcode }) => {
const ios = xcode({
version: "27.0",
// The snapshot tests check the device name, so the throwaway simulator
// has to use exactly this one.
simulator: {
name: "iPhone 18 Pro Max",
deviceType: "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro-Max",
runtime: "com.apple.CoreSimulator.SimRuntime.iOS-27-0",
},
});

return {
// SwiftLint and swift-format build into Vault/.build; keeping it makes
// lint incremental.
worktree: { keep: ["Vault/.build"] },
setup: [ios.setup],
checks: [
{ name: "Lint", run: ["make", "-C", "Vault", "lint"] },
{
name: "Fastlane config",
async skip(ctx) {
const wanted = (await Bun.file(`${ctx.worktree}/.ruby-version`).text()).trim();
const { stdout } = await ctx.capture(["ruby", "-e", "print RUBY_VERSION"], { env: await rubyEnv(ctx) });
return stdout === wanted ? undefined : `Ruby ${wanted} from .ruby-version isn't installed`;
},
async run(ctx) {
const env = await rubyEnv(ctx);
await ctx.exec(["bundle", "install", "--quiet"], { env });
await ctx.exec(["bundle", "exec", "ruby", "-c", "fastlane/Fastfile"], { env });
await ctx.exec(["bundle", "exec", "fastlane", "lanes"], { env });
},
},
ios.buildForTesting({
name: "Build",
workspace: "Vault.xcworkspace",
scheme: "CI_iOS",
testPlan: "iOSAllTests",
flags: ["-skipMacroValidation", "-skipPackagePluginValidation"],
}),
ios.testWithoutBuilding(),
],
};
}) satisfies ConfigFunction;

/**
* Puts rbenv's shims first on PATH. Shells that haven't run `rbenv init`
* (non-interactive ones, like an agent's) would otherwise find the system Ruby
* and skip the Fastlane check.
*/
async function rubyEnv(ctx: Context): Promise<Record<string, string>> {
const { exitCode, stdout } = await ctx.capture(["rbenv", "root"]);
return exitCode === 0 ? { PATH: `${stdout.trim()}/shims:${process.env.PATH}` } : {};
}
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"private": true,
"scripts": {
"validate": "local-check",
"prepare": "local-check install-hook"
},
"devDependencies": {
"@badbundle/local-check": "github:badbundle/local-check#v0.1.0"
}
}
Loading