ci: add Gitea Actions CI/CD, branch workflow, and contributor docs #25
86
.gitea/workflows/ci.yml
Normal file
86
.gitea/workflows/ci.yml
Normal file
@@ -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
|
||||||
86
.gitea/workflows/release.yml
Normal file
86
.gitea/workflows/release.yml
Normal file
@@ -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."
|
||||||
61
CONTRIBUTING.md
Normal file
61
CONTRIBUTING.md
Normal file
@@ -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.
|
||||||
45
ExportOptions.plist
Normal file
45
ExportOptions.plist
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
ExportOptions template for `xcodebuild -exportArchive`.
|
||||||
|
|
||||||
|
FILL IN:
|
||||||
|
• teamID — your Apple Developer Team ID. Detected from project.yml
|
||||||
|
(DEVELOPMENT_TEAM): K8BLMMR883 ← pre-filled below, verify it.
|
||||||
|
• method — choose ONE:
|
||||||
|
app-store → TestFlight / App Store submission
|
||||||
|
ad-hoc → distribute to registered UDIDs
|
||||||
|
development → internal dev devices
|
||||||
|
enterprise → in-house (Apple Enterprise Program only)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
• signingStyle "automatic" lets Xcode manage certs/profiles (pairs with
|
||||||
|
-allowProvisioningUpdates in release.yml). Switch to "manual" if you
|
||||||
|
install a specific distribution profile on the runner.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>app-store</string>
|
||||||
|
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>K8BLMMR883</string>
|
||||||
|
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>automatic</string>
|
||||||
|
|
||||||
|
<key>uploadSymbols</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>uploadBitcode</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<!-- Optional: pin the app to a specific provisioning profile (manual signing).
|
||||||
|
<key>provisioningProfiles</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.kutesir.KisaniCal</key>
|
||||||
|
<string>YOUR_PROFILE_NAME</string>
|
||||||
|
</dict>
|
||||||
|
-->
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
82
scripts/gitea-setup.sh
Executable file
82
scripts/gitea-setup.sh
Executable file
@@ -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 <<JSON
|
||||||
|
{
|
||||||
|
"branch_name": "main",
|
||||||
|
"rule_name": "main",
|
||||||
|
"enable_push": false,
|
||||||
|
"enable_push_whitelist": false,
|
||||||
|
"required_approvals": 1,
|
||||||
|
"enable_approvals_whitelist": false,
|
||||||
|
"block_on_rejected_reviews": true,
|
||||||
|
"dismiss_stale_approvals": true,
|
||||||
|
"enable_status_check": true,
|
||||||
|
"status_check_contexts": ["${STATUS_CONTEXT}"],
|
||||||
|
"block_on_outdated_branch": true
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
)
|
||||||
|
|
||||||
|
if curl -fsS -X POST "${AUTH[@]}" \
|
||||||
|
"${API}/repos/${OWNER}/${REPO}/branch_protections" \
|
||||||
|
-d "${PROTECT_PAYLOAD}" >/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."
|
||||||
Reference in New Issue
Block a user