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:
118
.github/workflows/publish-fdroid.yml
vendored
Normal file
118
.github/workflows/publish-fdroid.yml
vendored
Normal 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
3
.gitignore
vendored
@@ -11,4 +11,5 @@ ios/Pods/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
keystores/
|
keystores/
|
||||||
*.jks
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
18
.tasks/5/STATE.md
Normal file
18
.tasks/5/STATE.md
Normal 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
8
.tasks/5/decisions.md
Normal 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
47
.tasks/5/design.md
Normal 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
32
.tasks/5/plan.md
Normal 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
67
.tasks/5/review.md
Normal 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
27
.tasks/5/test-report.md
Normal 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.
|
||||||
Reference in New Issue
Block a user