diff --git a/docs/CICD.md b/docs/CICD.md new file mode 100644 index 0000000..9d31580 --- /dev/null +++ b/docs/CICD.md @@ -0,0 +1,75 @@ +# CI/CD Flow + +How a change travels from a local branch to TestFlight, and where the gates are. + +```mermaid +flowchart TD + A["feature/* branch · local
pre-push hook blocks main"] + B["Pull request → develop
develop is the default branch"] + C["CI · ci.yml (macOS runner)
xcodebuild build + test"] + D{"Merge gate
CI green + 1 approval"} + E["Pull request develop → main
same gate"] + F["Release · release.yml
archive → IPA → TestFlight"] + + A -->|git push · open PR| B + B -->|PR triggers CI| C + C -->|status check: build-and-test| D + D -->|promote when ready| E + E -->|on merge to main| F +``` + +## Steps + +1. **`feature/*` branch (local).** Branch off `develop`. A local + `.git/hooks/pre-push` hook rejects any direct push to `main`, so nothing + reaches the protected branch outside a PR. + +2. **PR → `develop`.** `develop` is the default branch and the integration + target for all feature work. + +3. **CI runs** — [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml) on the + self-hosted **macOS runner**. It runs `xcodebuild build` then + `xcodebuild test` (the `KisaniCalTests` target on an iOS simulator). The + result is published as a commit status named **`build-and-test`**. + +4. **Merge gate.** Gitea branch protection blocks the merge until: + - the `build-and-test` status check is **green**, + - there is **≥ 1 approval**, + - there are no rejected reviews, and + - the branch is up to date with its base. + + The same gate guards both `develop` and `main`. + +5. **Promote `develop` → `main`** via a second PR when a release is ready. It + passes through the identical gate. + +6. **Release runs** — [`.gitea/workflows/release.yml`](../.gitea/workflows/release.yml) + fires **only** on push to `main` (i.e. after a PR merges). It archives, + exports an IPA via [`ExportOptions.plist`](../ExportOptions.plist), and + uploads to **TestFlight** (currently a commented-out placeholder awaiting the + App Store Connect API-key secrets). + +## Two automation halves + +- **CI** (steps 3–4) gates **every** PR into `main`/`develop`. +- **Release** (step 6) runs **only** on `main`. + +Blue/manual steps are actions you take; CI and Release run unattended on the +runner. + +## Requirements & gotchas + +- **Runner labels.** Both workflows use `runs-on: [self-hosted, macOS]`. The + registered runner must carry **both** labels or jobs queue forever. Check at + Settings → Actions → Runners. +- **Status-check name.** Branch protection requires a check literally named + `build-and-test` (the CI job name). If the rendered context differs, update + the rule (`scripts/gitea-setup.sh`, `STATUS_CONTEXT`). +- **Simulator.** The runner needs an installed iOS runtime (Xcode → Settings → + Components) — CI auto-selects the first available iPhone simulator. +- **Signing.** CI builds with `CODE_SIGNING_ALLOWED=NO`. The Release job needs a + distribution cert + profile in the runner's login keychain. +- **Build number.** `CURRENT_PROJECT_VERSION` in `project.yml` must increase for + every TestFlight upload — App Store Connect rejects a repeated build number. + +See also [CONTRIBUTING.md](../CONTRIBUTING.md) for the branch workflow.