diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 000000000..972a657e1 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,27 @@ +#!/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" \ + >"$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 + +exit 0 diff --git a/.github/workflows/validate-all.yml b/.github/workflows/validate-all.yml deleted file mode 100644 index 8ec2722a1..000000000 --- a/.github/workflows/validate-all.yml +++ /dev/null @@ -1,203 +0,0 @@ -# These jobs are run on everything, including main and PRs. -# -# TEMPORARILY DISABLED — automatic triggers are commented out below. -# -# The project builds and tests against Xcode 27 / iOS 27.0, and the snapshot -# references are recorded on that runtime. No GitHub-hosted GA image provides -# Xcode 27 yet: `macos-26` tops out at Xcode 26.6 and carries no iOS 27.0 -# simulator runtime. The beta `xcode-27` image does have both, but its runner -# pool could not absorb this 14-way test matrix — every shard sat queued -# indefinitely. -# -# RE-ENABLE when Xcode 27 is available on the `macos-26` runner image (or a -# `macos-27` GA image ships): uncomment the `pull_request` and `push` triggers -# below. Everything else in this file is already configured for Xcode 27 and -# iOS 27.0, so no other change should be needed. -# -# Track availability at https://github.com/actions/runner-images -# -# Until then the workflow can still be run on demand via `workflow_dispatch` -# (Actions tab → Validate → Run workflow, or `gh workflow run validate-all.yml`). -name: Validate - -on: - workflow_dispatch: - # pull_request: - # branches: [main] - # push: - # branches: [main] - -env: - XCODE_VERSION: "27.0" - -jobs: - release-config: - name: Release Config - timeout-minutes: 5 - runs-on: macos-26 - - steps: - - uses: actions/checkout@v6.0.3 - - # The repo Actions allowlist permits GitHub-owned actions only, so this - # uses the Ruby version already installed on the macOS runner. - - name: Install Ruby dependencies - run: | - ruby -v - bundle install - - - name: Validate Ruby and Fastlane config - run: | - ruby -v - bundle exec ruby -c fastlane/Fastfile - bundle exec fastlane --version - bundle exec fastlane lanes - - lint: - name: Lint - timeout-minutes: 2 - runs-on: macos-26 - defaults: - run: - working-directory: ./Vault - - steps: - - uses: actions/checkout@v6.0.3 - - run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer - - - name: Cache SPM Dependencies - uses: actions/cache@v5.0.5 - with: - path: | - ~/Library/Caches/org.swift.swiftpm/repositories - Vault/.build - key: ${{ runner.os }}-spm-lint-xcode-${{ env.XCODE_VERSION }}-${{ hashFiles('Vault/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm-lint-xcode-${{ env.XCODE_VERSION }}- - - - name: Check Linting & Formatting - run: make lint - - ci-ios-build: - name: CI_iOS Build - timeout-minutes: 20 - runs-on: macos-26 - - steps: - - uses: actions/checkout@v6.0.3 - - run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer - - - name: Log Xcode Version - run: xcodebuild -version - - - name: Cache SPM Dependencies - uses: actions/cache@v5.0.5 - with: - path: ~/Library/Caches/org.swift.swiftpm/repositories - key: ${{ runner.os }}-spm-ci-ios-xcode-${{ env.XCODE_VERSION }}-${{ hashFiles('Vault/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm-ci-ios-xcode-${{ env.XCODE_VERSION }}- - - - name: Build CI_iOS - run: | - xcodebuild build-for-testing \ - -workspace Vault.xcworkspace \ - -scheme CI_iOS \ - -testPlan iOSAllTests \ - -destination 'platform=iOS Simulator,name=iPhone 18 Pro Max,OS=27.0' \ - -derivedDataPath "$RUNNER_TEMP/ci-ios-derived-data" \ - -skipMacroValidation \ - -skipPackagePluginValidation - - - name: Package CI_iOS Test Products - run: tar -czf "$RUNNER_TEMP/ci-ios-test-products.tgz" -C "$RUNNER_TEMP/ci-ios-derived-data/Build" Products - - - name: Upload CI_iOS Test Products - uses: actions/upload-artifact@v7.0.1 - with: - name: ci-ios-test-products - path: ${{ runner.temp }}/ci-ios-test-products.tgz - if-no-files-found: error - retention-days: 1 - - ci-ios-tests: - name: CI_iOS Tests (${{ matrix.suite }}) - timeout-minutes: 20 - runs-on: macos-26 - needs: ci-ios-build - strategy: - fail-fast: false - matrix: - include: - - suite: VaultiOSTests - only_testing: VaultiOSTests - - suite: VaultSettingsTests - only_testing: VaultSettingsTests - - suite: FoundationExtensionsTests - only_testing: FoundationExtensionsTests - - suite: ImageToolsTests - only_testing: ImageToolsTests - - suite: VaultiOSAutofillTests - only_testing: VaultiOSAutofillTests - - suite: VaultiOSWidgetsTests - only_testing: VaultiOSWidgetsTests - - suite: VaultFeedTests - only_testing: VaultFeedTests - - suite: VaultKeygenTests - only_testing: VaultKeygenTests - - suite: VaultCoreTests - only_testing: VaultCoreTests - - suite: VaultKeygenSpeedtestCompileTests - only_testing: VaultKeygenSpeedtestCompileTests - - suite: VaultAppIconGeneratorTests - only_testing: VaultAppIconGeneratorTests - - suite: VaultExportTests - only_testing: VaultExportTests - - suite: VaultBackupTests - only_testing: VaultBackupTests - - suite: CryptoEngineTests - only_testing: CryptoEngineTests - - steps: - - uses: actions/checkout@v6.0.3 - - run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer - - - name: Download CI_iOS Test Products - uses: actions/download-artifact@v8.0.1 - with: - name: ci-ios-test-products - path: ${{ runner.temp }} - - - name: Run CI_iOS Tests - run: | - mkdir -p "$RUNNER_TEMP/ci-ios-derived-data/Build" - tar -xzf "$RUNNER_TEMP/ci-ios-test-products.tgz" -C "$RUNNER_TEMP/ci-ios-derived-data/Build" - - XCTESTRUN="$(find "$RUNNER_TEMP/ci-ios-derived-data/Build/Products" -maxdepth 1 -name '*.xctestrun' -print -quit)" - - if [[ -z "$XCTESTRUN" ]]; then - echo "No .xctestrun file found in downloaded CI_iOS test products." - exit 1 - fi - - xcodebuild test-without-building \ - -xctestrun "$XCTESTRUN" \ - -destination 'platform=iOS Simulator,name=iPhone 18 Pro Max,OS=27.0' \ - -only-testing:${{ matrix.only_testing }} \ - -parallel-testing-enabled NO - - ci-ios: - name: CI_iOS - timeout-minutes: 5 - runs-on: ubuntu-latest - needs: [ci-ios-build, ci-ios-tests] - if: always() - - steps: - - name: Check CI_iOS Shards - run: | - if [[ "${{ needs.ci-ios-build.result }}" != "success" || "${{ needs.ci-ios-tests.result }}" != "success" ]]; then - echo "CI_iOS build result: ${{ needs.ci-ios-build.result }}" - echo "CI_iOS tests result: ${{ needs.ci-ios-tests.result }}" - exit 1 - fi diff --git a/AGENTS.md b/AGENTS.md index d19adf18a..d1dc2c1f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,10 @@ This root-level file only covers things that apply across the whole repo (the Sw Read [`MANIFESTO.md`](./MANIFESTO.md) before proposing or implementing any feature that touches killphrases, search passphrases, lock state, authentication, telemetry, backups, exports, or anything in the Danger Zone. The manifesto is normative — when a proposed change conflicts with it, the manifesto wins unless it is amended first via a dedicated `MANIFESTO:` PR. +## 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). + ## Layout - [`Vault/`](./Vault) — Swift Package with all targets, tests, and tooling. Open `Vault.xcworkspace` to work on it. diff --git a/README.md b/README.md index ecf968d39..a5370a5e4 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,20 @@ As soon as we are able, we will be dropping the xcodeproj project wrapper and go - `Vault.xcworkspace` what you should open - `/Vault` Swift Package that defines targets used by the app, build settings, tooling. - `/VaultApp` minimal wrapper that packages this into an executable application. + +### Validation + +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` +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: + +- `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 + +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/`. + +The check is self-attested: it records that the commit passed on the machine that posted it, rather than on independent CI. diff --git a/Vault/AGENTS.md b/Vault/AGENTS.md index bb05f4de0..0b0fa10ce 100644 --- a/Vault/AGENTS.md +++ b/Vault/AGENTS.md @@ -13,3 +13,12 @@ Use the simulator configuration specified in `README.md` for all builds and test ## Committing Before every commit, run `make format` and `make lint` from the `Vault/` directory to ensure code is properly formatted and passes linting. + +## 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)). + +- 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. diff --git a/Vault/Makefile b/Vault/Makefile index 905386f37..35349038e 100644 --- a/Vault/Makefile +++ b/Vault/Makefile @@ -20,6 +20,12 @@ 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. +.PHONY: validate +validate: + ../scripts/validate.sh + # Marketing screenshots: capture raw shots of the app on throwaway simulators, # then put them in device frames with titles. See Screenshots/README.md. .PHONY: screenshots diff --git a/Vault/README.md b/Vault/README.md index d0e0454de..8804e458a 100644 --- a/Vault/README.md +++ b/Vault/README.md @@ -125,6 +125,10 @@ You shouldn't need to manually change the locale or any other simulator setting