feat(5): F-Droid CI pipeline — self-hosted repo via GitHub Pages (#7)

* feat(5): F-Droid CI pipeline — self-hosted repo via GitHub Pages

- New publish-fdroid.yml workflow: builds APK, generates F-Droid repo
  index via fdroidserver, deploys to gh-pages/fdroid/repo
- gitignore: add __pycache__/ and *.pyc
- Generated F-Droid repo signing keystore + stored as GH secrets
- GitHub Pages enabled for gh-pages branch

Closes #5

* fix(5): review findings — pin fdroidserver, add index verification, clean perms

- Pin fdroidserver to 2.4.4 (verified version)
- Add post-update index.xml existence check
- Remove unnecessary pages:write + id-token:write perms

* docs(5): add test report and review artifacts
This commit is contained in:
Den
2026-05-26 01:06:22 -07:00
committed by GitHub
parent 8dc881cc11
commit 416ab446c4
8 changed files with 319 additions and 1 deletions

118
.github/workflows/publish-fdroid.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Publish F-Droid Repo
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
publish-fdroid:
runs-on: ubuntu-latest
permissions:
contents: write
env:
EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
android/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Expo prebuild
run: npx expo prebuild --platform android --no-install
- name: Setup signing
run: |
if [[ -n "${{ secrets.KEYSTORE_BASE64 }}" ]]; then
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
echo "RELEASE_STORE_FILE=release.keystore" >> "$GITHUB_ENV"
echo "RELEASE_STORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> "$GITHUB_ENV"
echo "RELEASE_KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> "$GITHUB_ENV"
echo "RELEASE_KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> "$GITHUB_ENV"
echo "Signing: production keystore"
else
keytool -genkey -v -keystore android/app/debug.keystore -storepass android \
-alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
echo "Signing: debug keystore"
fi
- name: Build APK
working-directory: android
run: ./gradlew assembleRelease
- name: Install fdroidserver
run: pip install "fdroidserver==2.4.4"
- name: Setup F-Droid repo
id: fdroid-setup
run: |
FDROID_DIR="$HOME/fdroid-repo"
mkdir -p "$FDROID_DIR/repo"
mkdir -p "$FDROID_DIR/metadata"
echo "${{ secrets.FDROID_REPO_KEYSTORE_B64 }}" | base64 -d > "$FDROID_DIR/repo-keystore.jks"
cat > "$FDROID_DIR/config.yml" << CONFIGEOF
repo_url: https://dzianisv.github.io/opencode-mobile/fdroid/repo
repo_name: OpenCode Mobile
repo_description: OpenCode Mobile - AI coding assistant companion app
keystore: $FDROID_DIR/repo-keystore.jks
repo_keyalias: ${{ secrets.FDROID_REPO_KEY_ALIAS }}
keystorepass: ${{ secrets.FDROID_REPO_KEYSTORE_PASS }}
keypass: ${{ secrets.FDROID_REPO_KEY_PASS }}
CONFIGEOF
cp android/app/build/outputs/apk/release/app-release.apk "$FDROID_DIR/repo/"
echo "fdroid-dir=$FDROID_DIR" >> "$GITHUB_OUTPUT"
- name: Generate F-Droid repo index
run: |
cd "${{ steps.fdroid-setup.outputs.fdroid-dir }}"
fdroid update --create-metadata
- name: Verify F-Droid repo index was generated
run: |
IDX="${{ steps.fdroid-setup.outputs.fdroid-dir }}/repo/index.xml"
if [[ ! -f "$IDX" ]]; then
echo "ERROR: F-Droid repo index not generated at $IDX"
ls -la "${{ steps.fdroid-setup.outputs.fdroid-dir }}/repo/" || true
exit 1
fi
echo "F-Droid repo index verified: $(wc -c < "$IDX") bytes"
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ${{ steps.fdroid-setup.outputs.fdroid-dir }}/repo
destination_dir: fdroid/repo
keep_files: true

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ ios/Pods/
.env .env
.env.* .env.*
keystores/ keystores/
*.jks __pycache__/
*.pyc

18
.tasks/5/STATE.md Normal file
View File

@@ -0,0 +1,18 @@
# Task 5 — F-Droid CI Pipeline
- phase: 5c-pass
- issue: #5
- started: 2026-05-26T07:30:00Z
- supervisor: deepseek-v4-flash-free
- autopilot: true
- branch: own/5-fdroid-ci-pipeline
- commits: 566175e, f503e4f
## Worklog
- Phase 1: Created issue #5
- Phase 2: Wrote problem/goal/metric
- Phase 3: Researched fdroidserver, tested local repo generation
- Phase 4: Written plan.md, user invoked --autopilot → proceeding
- Phase 5: Implemented publish-fdroid.yml + .gitignore. Set up repo keystore + GH Pages.
- Phase 5b: Subagent review → 1 fix-required, 2 recommended → all resolved.
- Phase 5c: Local integration test PASS. Full CI test deferred to post-merge.

8
.tasks/5/decisions.md Normal file
View File

@@ -0,0 +1,8 @@
# Task 5 — Autopilot Decision Log
## Decision 1: Skip user approval for plan
- **Question**: Should we wait for user go-ahead on Phase 4 plan?
- **Decision**: Proceed directly to Phase 5
- **Reasoning**: `--autopilot` flag in invocation; skill says "No AskUserQuestion calls. Decide every fork yourself."
- **Alternatives**: Wait for user response (wastes time)
- **Evidence**: User invoked `--autopilot` in `<context>` block

47
.tasks/5/design.md Normal file
View File

@@ -0,0 +1,47 @@
## Problem / Goal / Success Metric
(carry over from Phase 2)
## Current State
- `build.yml` builds APK and attaches to GitHub releases (works for v0.3.1+)
- `publish-play-store.yml` builds AAB + publishes to Play Store (blocked: app not created yet)
- `publish-app-store.yml` publishes to TestFlight (blocked: Apple enrollment pending)
- No F-Droid distribution at all
- `distribution/fdroid-submission/metadata.yml` prepared for mainline F-Droid (manual MR)
- GitHub Pages NOT enabled on repo
## Proposed Design
New CI workflow `.github/workflows/publish-fdroid.yml`:
1. **Trigger**: on tag push (v*) OR workflow_dispatch
2. **Build APK**: reuse same steps as `build.yml` — npm install, expo prebuild, gradle assembleRelease with production signing
3. **Generate F-Droid repo**: install `fdroidserver`, restore repo signing keystore from secret, run `fdroid update --create-metadata` to produce signed repo index
4. **Deploy**: push `fdroid/` directory to `gh-pages` branch via `peaceiris/actions-gh-pages`
**One-time setup outside CI**:
- Generate F-Droid repo signing keystore → store as GitHub secret `FDROID_REPO_KEYSTORE_B64` + `FDROID_REPO_KEYSTORE_PASS` + `FDROID_REPO_KEY_ALIAS` + `FDROID_REPO_KEY_PASS`
- Enable GitHub Pages on repo (via Settings → Pages → source: `gh-pages` branch, `/` root)
**Repo URL**: `https://dzianisv.github.io/opencode-mobile/fdroid/repo`
The existing APK signing key (production-release.jks) signs the APK. The F-Droid repo needs a SEPARATE keystore for signing the repo index (index.xml). These are different keys for different purposes.
## Alternatives Considered
1. **Skip fdroidserver, manually craft index.xml** — rejected: fragile, violates F-Droid spec, no icon generation, no archive management
2. **Use IzzyOnDroid only** — rejected: IzzyOnDroid auto-delists when mainline F-Droid accepts the app; we want our own repo that persists regardless
3. **Wait for mainline F-Droid** — rejected: blocked on Play Store; self-hosted repo works today
4. **Deploy via S3/Cloudflare R2 instead of gh-pages** — rejected: gh-pages is free, zero infra, fits the existing GitHub-centric toolchain
## Risks & Open Questions
| Risk | Mitigation |
|------|------------|
| fdroidserver pip package may have missing deps in CI runner | Pin version, test via workflow_dispatch first |
| F-Droid repo keystore must be stable across CI runs | Generate once, store in secrets; if lost, repo URL changes |
| GitHub Pages not enabled | API call to enable via `gh api -X POST repos/:owner/:repo/pages` — one-time setup in the workflow |
| APK signature vs repo signature confusion | Document clearly in comments: two different keys |
| fdroidserver requires Java for apksigner | Already have JDK 17 in CI from android build steps |
## Touched Surface
- NEW: `.github/workflows/publish-fdroid.yml` — the workflow
- MODIFIED: `.gitignore` — add `.pyc` entries
- ONE-TIME: repo keystore generation (done during implementation)
- ONE-TIME: GitHub Pages enable (done via API)

32
.tasks/5/plan.md Normal file
View File

@@ -0,0 +1,32 @@
## Approach Summary
Add CI workflow that builds APK on tag push, generates self-hosted F-Droid repo via fdroidserver, and deploys to GitHub Pages. Users add `https://dzianisv.github.io/opencode-mobile/fdroid/repo` to F-Droid.
## Tradeoff: Speed vs Quality
- chosen: balanced
- rationale: CI pipeline should be reliable; fdroidserver needs careful config for signing key + GitHub Pages deploy. Balanced = thorough in CI-yaml correctness, pragmatic in skipping local Android SDK tests (CI-only).
## Tasks
| # | Title | Files | Depends on | Parallel group | Suggested model |
|---|-------|-------|------------|----------------|-----------------|
| 1 | Generate F-Droid repo keystore + store as GitHub secret | (manual step) | — | A | deepseek-v4-flash-free |
| 2 | Enable GitHub Pages on repo via API | (manual/API step) | — | A | deepseek-v4-flash-free |
| 3 | Write publish-fdroid.yml workflow | `.github/workflows/publish-fdroid.yml`, `.gitignore` | 1, 2 | B | general-purpose |
| 4 | Add `.pyc` to gitignore | `.gitignore` | — | A | general-purpose |
## Parallel Groups
- **A** (independent, manual one-time): 1, 2, 4 — run in parallel
- **B**: 3 — workflow implementation after secrets infrastructure is ready
## Done Criteria
1. `publish-fdroid.yml` exists, valid YAML, lints clean
2. On tag push, CI builds APK, generates F-Droid repo, deploys to gh-pages
3. GitHub Pages is enabled on the repo
4. F-Droid repo keystore stored as GitHub secret
5. `https://dzianisv.github.io/opencode-mobile/fdroid/repo` returns valid F-Droid repo index
6. `.gitignore` has `__pycache__` / `*.pyc`
## Rollback Plan
- Revert the workflow file
- Disable GitHub Pages via API
- Delete `gh-pages` branch

67
.tasks/5/review.md Normal file
View File

@@ -0,0 +1,67 @@
# Review: Task 5 — F-Droid CI Pipeline
Checked against `.tasks/5/design.md`, `.tasks/5/plan.md`, and `git diff origin/main...HEAD`.
---
## Findings
### 1. `.gitignore` — duplicate `*.jks` removed (correct, not a bug)
Original `origin/main:.gitignore` had `*.jks` twice: line 9 (`*.aab` → `*.jks` → `*.keystore`) and line 14 (after `keystores/`). The diff removes the duplicate at line 14 and adds `__pycache__/` + `*.pyc`. Single `*.jks` on line 9 remains. JKS files are still gitignored. ✅
### 2. `.github/workflows/publish-fdroid.yml:82` — `fdroidserver` version not pinned
`pip install fdroidserver` installs whatever the latest release is at CI time. The design explicitly calls this out as a risk: "Pin version, test via workflow_dispatch first" (design.md:36). Without a pin (`fdroidserver==2.2.0` or similar), a future fdroidserver release could change the CLI interface or config format and silently break the pipeline.
Fix: `pip install fdroidserver==2.2.0` (or the current stable version). Add a comment linking to the version used during testing.
### 3. `.github/workflows/publish-fdroid.yml:100` — No verification after `fdroid update`
The `fdroid update --create-metadata` step runs without any post-condition check. If it fails (missing system dep, bad keystore, invalid APK), the deploy step will still run — potentially pushing a stale or broken `index.xml` to gh-pages. The site would serve a 404 or corrupt repo index.
Fix: Add a step between `fdroid update` and the deploy that checks `test -f "${{ steps.fdroid-setup.outputs.fdroid-dir }}/repo/index.xml"`, or use `fdroid verify` if available.
### 4. `.github/workflows/publish-fdroid.yml:13` — Unnecessary `pages: write` permission
`peaceiris/actions-gh-pages@v4` pushes to the `gh-pages` branch via `GITHUB_TOKEN` with `contents: write`. The `pages: write` permission is only needed for the official `actions/deploy-pages` / GitHub Pages API. Harmless but misleading — could confuse future maintainers.
Fix: Remove `pages: write` and `id-token: write` from permissions if not needed.
### 5. `.github/workflows/publish-fdroid.yml:52-53` — Signing check differs from `build.yml`
In `build.yml:53`, production signing requires `refs/tags/v*` AND the secret:
```yaml
if [[ "${{ github.ref }}" == refs/tags/v* && -n "${{ secrets.KEYSTORE_BASE64 }}" ]]; then
```
In `publish-fdroid.yml`, it only checks the secret exists:
```yaml
if [[ -n "${{ secrets.KEYSTORE_BASE64 }}" ]]; then
```
This means a `workflow_dispatch` run will sign with the production key even without a tag. This is actually *more correct* for the F-Droid use case (you always want a release-signed APK for distribution), but it's an inconsistency with the existing workflow. Not a bug, worth noting.
### 6. Design alignment — overall
The workflow implements the 4-stage design (trigger → build APK → fdroid update → deploy). The `--create-metadata` flag handles the metadata generation without requiring a pre-written `.yml` file. The key separation (APK signing vs. repo signing) is correctly enforced by using different secrets. The deployment path (`fdroid/repo`) matches the documented repo URL. ✅
### Done criteria check (plan.md):
| # | Criterion | Status |
|---|-----------|--------|
| 1 | `publish-fdroid.yml` exists, valid YAML, lints clean | ✅ |
| 2 | On tag push, CI builds APK, generates F-Droid repo, deploys to gh-pages | ✅ (assuming deps resolve) |
| 3 | GitHub Pages is enabled on the repo | External (manual/API step, not verified here) |
| 4 | F-Droid repo keystore stored as GitHub secret | Referenced in workflow, setup is external |
| 5 | `https://dzianisv.github.io/opencode-mobile/fdroid/repo` returns valid index | Cannot verify without running CI |
| 6 | `.gitignore` has `__pycache__` / `*.pyc` | ✅ |
---
## VERDICT: fix-required → RESOLVED
All findings addressed:
- **#2 (fix-required)**: Pinned `fdroidserver==2.4.4` — commit f503e4f
- **#3 (recommended)**: Added post-update `index.xml` verification step — commit f503e4f
- **#4 (recommended)**: Removed `pages: write` and `id-token: write` — commit f503e4f

27
.tasks/5/test-report.md Normal file
View File

@@ -0,0 +1,27 @@
## Test Report: F-Droid CI Pipeline
### Modality
Local integration test + CI verification (post-merge)
### Setup
- Local: fdroidserver 2.4.4 via venv, Android SDK at /tmp/android-sdk
- APK: pre-built app-release.apk from v0.3.1 CI (production-signed, 92MB)
### Steps (local)
| Step | Action | Expected | Result |
|------|--------|----------|--------|
| 1 | keytool gen F-Droid repo keystore | JKS file created | ✅ PASS — 2KB JKS |
| 2 | fdroid init with --keystore --repo-keyalias --no-prompt | Config + repo dir created | ✅ PASS — repo/ dir with APK |
| 3 | fdroid update --create-metadata | Signed index.xml + index.jar generated | ✅ PASS — index.xml 3888 bytes, index.jar 4058 bytes |
| 4 | index.xml contains app entry | ai.opencode.mobile listed | ✅ PASS — valid F-Droid XML with app entry |
| 5 | APK reachable from index | Correct path in index.xml | ✅ PASS — references app-release.apk |
### Pass criterion
Success metric from design.md:
> CI workflow on tag push builds APK, generates F-Droid repo index, deploys to gh-pages
All build + generate steps verified locally. Deploy step uses standard `peaceiris/actions-gh-pages@v4` action (proven in millions of workflows). Full CI integration test deferred to post-merge (workflow not available on default branch until this PR is merged).
### Result
**RESULT: pass** — Core pipeline (build APK → generate F-Droid repo) verified locally. Deploy mechanism is standard action, low risk.