From cf5dc8943a9fda202ed1c588b96635554badac0d Mon Sep 17 00:00:00 2001 From: kutesir Date: Fri, 3 Jul 2026 01:38:22 +0300 Subject: [PATCH] ci: add Gitea Actions CI/CD, branch workflow, and contributor docs - .gitea/workflows/ci.yml: build + test KisaniCal scheme on a self-hosted macOS runner for every PR into main/develop (and pushes to develop). - .gitea/workflows/release.yml: archive + export IPA on push to main, with a commented TestFlight upload placeholder (App Store Connect API key). - ExportOptions.plist: export template (Team ID K8BLMMR883, app-store method). - scripts/gitea-setup.sh: idempotent Gitea API setup for develop default branch + main branch protection. - CONTRIBUTING.md: feature/* -> develop -> main workflow and PR gate rules. Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/ci.yml | 86 ++++++++++++++++++++++++++++++++++++ .gitea/workflows/release.yml | 86 ++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 61 +++++++++++++++++++++++++ ExportOptions.plist | 45 +++++++++++++++++++ scripts/gitea-setup.sh | 82 ++++++++++++++++++++++++++++++++++ 5 files changed, 360 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 CONTRIBUTING.md create mode 100644 ExportOptions.plist create mode 100755 scripts/gitea-setup.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..954192e --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +# Build + test on every PR into main or develop (and on direct pushes to develop). +on: + pull_request: + branches: [main, develop] + push: + branches: [develop] + +# ⚠️ RUNNER LABELS — must match how your Mac runner was registered with act_runner. +# Check your runner's labels in Gitea: Settings → Actions → Runners (click the runner). +# If it registered as e.g. `macos` or `self-hosted`, change `runs-on` below to match. +# `[self-hosted, macOS]` means "a runner that has BOTH labels". +jobs: + build-and-test: + runs-on: [self-hosted, macOS] + + env: + SCHEME: KisaniCal + PROJECT: KisaniCal.xcodeproj + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # The .xcodeproj is generated from project.yml via XcodeGen. + # Regenerate it so CI never builds a stale project. No-op if xcodegen isn't installed. + - name: Regenerate Xcode project (XcodeGen) + run: | + if command -v xcodegen >/dev/null 2>&1; then + xcodegen generate + else + echo "xcodegen not found on runner — using committed ${PROJECT}." + echo "Install with: brew install xcodegen" + fi + + - name: Show toolchain + run: | + xcodebuild -version + xcrun simctl list runtimes | grep -i ios || true + + # Pick a concrete, bootable iOS Simulator (needed for `xcodebuild test`). + - name: Select iOS Simulator destination + id: sim + run: | + # First available iPhone simulator on any installed iOS runtime. + UDID=$(xcrun simctl list devices available -j | python3 -c "import json,sys,re; d=json.load(sys.stdin)['devices']; devs=[x for k,v in d.items() if re.search('iOS',k) for x in v if x.get('isAvailable') and 'iPhone' in x['name']]; print(devs[0]['udid'] if devs else '')") + if [ -z "$UDID" ]; then + echo "No available iPhone simulator found on the runner." >&2 + echo "Install an iOS runtime via Xcode → Settings → Components." >&2 + exit 1 + fi + echo "udid=$UDID" >> "$GITHUB_OUTPUT" + echo "Using simulator UDID: $UDID" + + - name: Build + run: | + set -o pipefail + xcodebuild build \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -destination "id=${{ steps.sim.outputs.udid }}" \ + CODE_SIGNING_ALLOWED=NO \ + | xcbeautify || xcodebuild build \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -destination "id=${{ steps.sim.outputs.udid }}" \ + CODE_SIGNING_ALLOWED=NO + + - name: Test + run: | + set -o pipefail + xcodebuild test \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -destination "id=${{ steps.sim.outputs.udid }}" \ + CODE_SIGNING_ALLOWED=NO \ + | xcbeautify || xcodebuild test \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -destination "id=${{ steps.sim.outputs.udid }}" \ + CODE_SIGNING_ALLOWED=NO diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..084ad33 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,86 @@ +name: Release + +# Runs only when code lands on main (i.e. after a PR is merged). +on: + push: + branches: [main] + +# ⚠️ Same runner-label caveat as ci.yml — adjust `runs-on` to match your registered runner. +jobs: + archive-and-export: + runs-on: [self-hosted, macOS] + + env: + SCHEME: KisaniCal + PROJECT: KisaniCal.xcodeproj + # A real distribution build MUST be signed. This requires an Apple + # Distribution certificate + provisioning profile installed in the + # runner's login keychain (see notes at the bottom of this file). + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Regenerate Xcode project (XcodeGen) + run: | + if command -v xcodegen >/dev/null 2>&1; then xcodegen generate; fi + + - name: Archive + run: | + set -o pipefail + xcodebuild archive \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -archivePath "$RUNNER_TEMP/KisaniCal.xcarchive" \ + -allowProvisioningUpdates + + - name: Export IPA + run: | + set -o pipefail + xcodebuild -exportArchive \ + -archivePath "$RUNNER_TEMP/KisaniCal.xcarchive" \ + -exportOptionsPlist ExportOptions.plist \ + -exportPath "$RUNNER_TEMP/export" \ + -allowProvisioningUpdates + ls -la "$RUNNER_TEMP/export" + + # ------------------------------------------------------------------ + # PLACEHOLDER: Upload to TestFlight / App Store Connect. + # + # Add these as Gitea repo secrets: Settings → Actions → Secrets + # APP_STORE_CONNECT_API_KEY_ID (the "Key ID" from App Store Connect) + # APP_STORE_CONNECT_ISSUER_ID (the "Issuer ID") + # APP_STORE_CONNECT_API_KEY_P8 (contents of the AuthKey_XXXX.p8 file) + # + # Then uncomment ONE of the approaches below. + # ------------------------------------------------------------------ + + # --- Option A: modern notarytool / App Store Connect API key (recommended) --- + # - name: Write API key to file + # run: | + # mkdir -p ~/private_keys + # echo "${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }}" > ~/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8 + # + # - name: Upload to TestFlight (altool with API key) + # run: | + # xcrun altool --upload-app \ + # --type ios \ + # --file "$RUNNER_TEMP/export/KisaniCal.ipa" \ + # --apiKey "${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}" \ + # --apiIssuer "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" + + # --- Option B: notarytool (for notarizing a macOS build, not iOS TestFlight) --- + # - name: Notarize + # run: | + # xcrun notarytool submit "$RUNNER_TEMP/export/KisaniCal.ipa" \ + # --key ~/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8 \ + # --key-id "${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}" \ + # --issuer "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \ + # --wait + + - name: Notice + run: | + echo "IPA exported. TestFlight upload step is a commented-out placeholder." + echo "Add App Store Connect API-key secrets and uncomment Option A in release.yml." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3aea7bd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing to KisaniCal + +## Branch workflow + +``` +feature/* ─PR→ develop ─PR→ main +``` + +- **`main`** — protected, release-ready. Never commit or push directly. Every + change arrives via a reviewed pull request. Pushes to `main` trigger the + release workflow (archive → IPA → TestFlight). +- **`develop`** — the default working branch and integration target. CI runs on + every PR into it. +- **`feature/*`** — short-lived branches for a single change, e.g. + `feature/voice-quick-add`, `fix/icloud-restore`. Branch off `develop`. + +## Day-to-day + +```bash +git checkout develop +git pull +git checkout -b feature/my-change + +# ...work, commit... + +git push -u origin feature/my-change +# then open a PR in Gitea: feature/my-change → develop +``` + +When `develop` is ready to ship, open a PR **`develop` → `main`**. + +## Pull request requirements + +A PR into `main` (and `develop`) cannot be merged until: + +1. **CI passes** — the `build-and-test` job in `.gitea/workflows/ci.yml` builds + the app and runs `KisaniCalTests` on an iOS Simulator. +2. **At least 1 approval** — reviewed and approved. +3. **No unresolved change requests** — a rejected review blocks the merge. +4. Your branch is **up to date** with the base branch. + +Direct pushes, force-pushes, and deletion of `main` are blocked server-side by +Gitea branch protection, and a local `pre-push` hook blocks accidental direct +pushes to `main` from this clone. + +## CI / CD + +- **CI** (`.gitea/workflows/ci.yml`) — runs on every PR into `main`/`develop` + and on pushes to `develop`. Builds + tests. A red check blocks the merge. +- **Release** (`.gitea/workflows/release.yml`) — runs only on push to `main` + (i.e. after a PR merges). Archives, exports an IPA via `ExportOptions.plist`, + and (once configured) uploads to TestFlight. + +Both workflows run on a **self-hosted macOS runner** with Xcode — there is no +Gitea-hosted Mac runner. + +## Versioning + +`MARKETING_VERSION` (e.g. `2.0`) and `CURRENT_PROJECT_VERSION` (build number) +live in `project.yml`. The build number must **increase** for every TestFlight +upload — App Store Connect rejects a build number it has already seen. diff --git a/ExportOptions.plist b/ExportOptions.plist new file mode 100644 index 0000000..7bd125e --- /dev/null +++ b/ExportOptions.plist @@ -0,0 +1,45 @@ + + + + + + method + app-store + + teamID + K8BLMMR883 + + signingStyle + automatic + + uploadSymbols + + + uploadBitcode + + + + + diff --git a/scripts/gitea-setup.sh b/scripts/gitea-setup.sh new file mode 100755 index 0000000..579409d --- /dev/null +++ b/scripts/gitea-setup.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# One-shot Gitea setup for KisaniCal: +# 1. Creates the `develop` branch on the server (from main) if missing. +# 2. Sets `develop` as the repo's default branch. +# 3. Adds branch protection on `main` (require PR + 1 approval + status check, +# block direct pushes / force pushes / deletion). +# +# USAGE: +# export GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxx # a Gitea access token with repo scope +# ./scripts/gitea-setup.sh +# +# Create the token in Gitea: click your avatar → Settings → Applications → +# "Generate New Token" (scopes: at least write:repository ). +# +set -euo pipefail + +GITEA_URL="http://10.10.1.21:3002" +OWNER="kutesir" +REPO="KisaniCal" + +# The commit-status context that must pass before merge into main. +# For Gitea Actions this is the JOB name from ci.yml ("build-and-test"). +# After your first CI run, confirm the exact context string at: +# ${GITEA_URL}/${OWNER}/${REPO}/commits (hover the check) and adjust if needed. +STATUS_CONTEXT="build-and-test" + +: "${GITEA_TOKEN:?Set GITEA_TOKEN env var first (see header).}" + +API="${GITEA_URL}/api/v1" +AUTH=(-H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json") + +echo "==> 1/3 Ensuring 'develop' branch exists on server..." +if curl -fsS "${AUTH[@]}" "${API}/repos/${OWNER}/${REPO}/branches/develop" >/dev/null 2>&1; then + echo " develop already exists." +else + curl -fsS -X POST "${AUTH[@]}" \ + "${API}/repos/${OWNER}/${REPO}/branches" \ + -d '{"new_branch_name":"develop","old_branch_name":"main"}' >/dev/null + echo " created develop from main." +fi + +echo "==> 2/3 Setting default branch to 'develop'..." +curl -fsS -X PATCH "${AUTH[@]}" \ + "${API}/repos/${OWNER}/${REPO}" \ + -d '{"default_branch":"develop"}' >/dev/null +echo " default branch = develop." + +echo "==> 3/3 Applying branch protection on 'main'..." +# If a rule already exists this POST returns 409; we then PUT-update it. +PROTECT_PAYLOAD=$(cat </dev/null 2>&1; then + echo " created protection rule for main." +else + echo " rule may already exist — updating it..." + curl -fsS -X PATCH "${AUTH[@]}" \ + "${API}/repos/${OWNER}/${REPO}/branch_protections/main" \ + -d "${PROTECT_PAYLOAD}" >/dev/null + echo " updated protection rule for main." +fi + +echo "" +echo "Done. Note: Gitea automatically blocks force-pushes and deletion of a" +echo "protected branch, so 'main' is now safe from both."