Makes Codeberg canonical for git, issues, PRs, tags and releases. The self-hosted Gitea instance stays build infrastructure: signing key, F-Droid publishing, release pipeline.
**This PR is its own test.** It is the first PR opened on Codeberg, so a green `CI` check proves the new runner works *and* that the submodule resolves from its new home.
### 1 · floret-kit moved
Mirrored to `jlmakiola/floret-kit` (6 branches, 3 tags, every SHA verified identical) and `.gitmodules` repointed. This is what actually unblocks contributors — a clone previously could not resolve its submodule without reaching the personal Gitea instance.
The Gitea copy is **kept**: every existing tag records the old submodule URL, so rebuilds of past releases (including F-Droid reproducible rebuilds) still resolve.
### 2 · Workflows split by directory
Forgejo's lookup is first-match-wins across `.forgejo/` → `.gitea/` → `.github/`, and Gitea cannot see `.forgejo/` at all. So each forge sees exactly one set, with no duplicated files and no expression to keep in sync:
| Directory | Runs on | Contains | Secrets |
| --- | --- | --- | --- |
| `.forgejo/workflows/` | Codeberg | `ci.yaml`, `translations.yaml` | **none** |
| `.gitea/workflows/` | Gitea | `release.yaml`, `renovate.yml` | all of them |
The line is drawn at **secrets, not CI-vs-release** — that is what makes fork PRs safe. Renovate deliberately does *not* move despite opening PRs here; it keeps running where its token already lives and merely talks to Codeberg's API.
### 3 · Two release-pipeline safety changes
- `release.yaml`'s `detect` gets an explicit `repository_owner` guard. The directory split only holds while `.forgejo/` is non-empty; empty it and Codeberg would fall back to `.gitea/` and start running the release pipeline on the contributor-facing runner, without secrets.
- `detect` now reads tags from **Codeberg**, not from the Gitea instance it runs on. Push mirroring is `git push --mirror`, so a tag minted on Gitea is deleted by the next sync until the Codeberg tag push propagates back — asking Gitea inside that window reports "no tag" for an already-shipped release and would cut it twice. It also now fails on any status other than 200/404 rather than reading a transient error as "no tag": a failed job is recoverable, a duplicate release is not.
### 4 · Links repointed
In-app Source/License links, README badge, both F-Droid metadata files. **`Repo:` in `docs/fdroid-official/` deliberately stays on Gitea** — it keeps receiving `main` and every tag, so it remains a complete build source, and leaving it alone means no fdroiddata MR and no reproducible-build risk.
### Not in this PR
Renovate + Weblate repointing, and the Codeberg → Gitea push mirror (browser-side).
Supersedes Gitea PR #104.
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/86
The Codeberg mirror step pushes the tag, then immediately POSTs the release
for it — but Codeberg 500s when the release request outruns its indexing of
the just-pushed ref, and with only one attempt that single 500 skipped the
mirror every release (the same POST succeeds seconds later, as a manual retry
confirmed for v2.16.0). Wrap the create/update in a backoff retry loop that
PATCHes in place if a release already exists, so a transient 5xx no longer
loses the mirror. Step stays best-effort (continue-on-error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Codeberg publish step (added but not yet exercised by a release)
would 500 the same way agendula's did: the tag is created via the Gitea
API, which the push mirror doesn't propagate promptly, so a release POST
with target_commitish outruns the mirror and fails on an unknown commit.
Push the tag straight to Codeberg from the runner, then create the
release against that existing tag with no target_commitish. Race-free.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Publish the signed release APK and a SHA-256 checksum as a Codeberg
release, a direct-download channel for users who don't want F-Droid.
Gitea already push-mirrors branches and tags to Codeberg, but releases
aren't git objects and don't sync, so the pipeline creates the release
over the Codeberg API and attaches calendula_v<version>.apk + .sha256.
Same app-signed APK the F-Droid repo serves, so no added trust surface.
Best-effort (continue-on-error) so a Codeberg outage never fails an
already-published F-Droid release; skips cleanly when CODEBERG_TOKEN is
unset. Upsert + same-name asset replacement keep re-runs safe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersedes the stale chore/floret-kit-core-time branch: re-applies the
floret-kit migration on top of current main (122 commits ahead of the old
branch's base), pinning the kit at the multi-value-reminders + pinned-picker HEAD.
- Submodule + composite build (includeBuild), 6 module deps, CI submodules:
recursive, reproducible-release scan extended to the kit, F-Droid recipe.
- Deletes the inline copies now owned by the kit (GroupedList, Picker scaffolds,
InlineTextField, OptionCard, DialogControls, CrashReporter + dialog/submit,
OnboardingScaffold, AppLanguage, TimeBridge, ReorderableColumn, DebugRibbon)
and redraws them from components/identity/core-crash/core-locale/core-time.
- Reminder overrides drawn from core-reminders (multi-value ReminderOverride +
codec); Calendula keeps its app-specific bits (all-day resolution, labels,
presets, the multi-select ReminderDefaultPicker, its own CrashReportActivity).
- Theme draws FloretExpressiveTheme while keeping the user-typography param.
Build pending (deferred): run ./gradlew :app:compileDebugKotlin with ANDROID_HOME
(or floret-kit/local.properties) set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make fastlane/metadata/android/ the single source of truth for store
listing metadata, consumed directly by the official F-Droid repo and
transformed into the self-hosted repo's localized layout at release time.
- Move fdroid-metadata/<appid>/<locale>/ -> fastlane/metadata/android/<locale>/
(git-tracked renames: summary->short_description, description->full_description,
+ title.txt, images/icon.png, images/phoneScreenshots/); keep the app-level
.yml control file for the self-hosted `fdroid update`.
- Add scripts/fastlane_to_fdroid_localized.sh (fastlane -> F-Droid localized,
incl. changelogs) and scripts/sync_changelog_to_fastlane.sh (CHANGELOG.md ->
fastlane changelog); verified byte-identical to the previous metadata.
- release.yaml: build self-hosted metadata from the fastlane tree and sync the
per-version changelog before the transform (one changelog source for both
channels).
- Disable AGP VCS-info embedding on release builds (vcsInfo { include = false })
so builds reproduce byte-for-byte vs the distributed APK — the only file that
otherwise differed (META-INF/version-control-info.textproto). Effective from
the next release.
- Add docs/fdroid-official/ (draft fdroiddata recipe: reproducible build +
AllowedAPKSigningKeys + Binaries + notes).
- Repoint README screenshots/icon, update docs/README + RELEASING, and skip the
Android build on fastlane-only changes (ci.yaml).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks the pipeline so a change is built once and a release is driven by the
merge, not a manual tag push.
- ci.yaml now runs on pull_request (one gate per PR) instead of every branch
push, so there's no CI-on-push + CI-on-merge double run. A single `ci` job
with a docs-only fast-path keeps the required "CI" check always reporting
(docs/metadata-only PRs skip the Android build but still go green).
- release.yaml triggers on push to main. A cheap `detect` job reads versionName
from build.gradle; only when no tag for it exists does the `release` job run:
tests on the merged commit, build + sign, publish to F-Droid, then create the
vX.Y.Z tag + Gitea release via the API (target_commitish = the merged sha).
The tag is now an OUTPUT of a successful release, not its trigger — a failure
before publish leaves no tag, so re-running safely retries. No more separate
tag-triggered run or duplicate ci job.
- The committed versionName/versionCode are now the source of truth (pipeline
pins versionCode from versionName); updated the build.gradle comment.
- translations.yaml switched to pull_request (same path filter).
- docs/RELEASING.md: release-by-merge flow, no manual git tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1.3 Versioning: the git tag is already the de-facto single source of truth
(every published versionCode uses MAJOR*10000+MINOR*100+PATCH; committed 13
was a stale outlier). Align the committed default to 20000 and document the
scheme in a comment + docs/RELEASING.md.
P1.4 F-Droid changelogs: a tag-only step extracts the tag's CHANGELOG section
into metadata/.../en-US/changelogs/<versionCode>.txt so clients show a
per-version "What's New". Also upload metadata/ (non-secret, never web-served)
alongside repo/ so changelog history survives across releases.
P1.5 R8 mapping: attach mapping-<version>.txt.gz to the Gitea release
(best-effort, continue-on-error) so user crash stacktraces stay
deobfuscatable. The gitea-release notes step is now an upsert (PATCH if the
release already exists) so it composes with the mapping step creating the
release first.
P1.6 docs/RELEASING.md: release ritual, versioning scheme, secrets inventory,
key custody/recovery, manual re-sign path, F-Droid repo details.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The release job assumed the ref is a version tag (Set version from git tag →
versionCode). A manual workflow_dispatch from a branch yielded versionCode 0
and Gradle aborted assembleRelease before the F-Droid steps ran.
Gate the tag-only steps (version, app keystore, assembleRelease, copy APK)
on refs/tags/*. On a manual dispatch the job now skips the APK build and just
re-signs the existing index with the configured repo key and re-uploads —
exactly what a repo-key rotation or recovery needs, no new release required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The F-Droid repo signing key (keystore.p12) and its config.yml — including
the keystore passwords in cleartext — were publicly downloadable at
apps.dev.jeanlucmakiola.de/dev/fdroid/ because the release workflow uploaded
the entire fdroid/ working dir into the web-served path. The webserver has
since been locked down to repo/ only; this rotates the now-compromised key
and removes the root cause.
- release.yaml: restore the repo key + config from new CI secrets
(FDROID_KEYSTORE_BASE64, FDROID_CONFIG_BASE64) instead of the box; upload
ONLY repo/ so the key never re-enters the served tree.
- release.yaml: fail loudly when the repo key secrets are unset, replacing
`fdroid update --create-key`, which silently minted a NEW repo key on a
wiped server and would have broken every user's pinned fingerprint.
- README: publish the new repo fingerprint (C2C0…3425). Existing users must
remove and re-add the repo.
- .gitignore: ignore *.p12 and the whole /fdroid/ working dir.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documentation pass after the 2.0 milestone:
- docs/ARCHITECTURE.md — principles (provider as single source of truth,
observer-driven UI, JVM-first tests, no network), layer + reminder
mermaid diagrams, navigation (overlay/held-key, no nav lib), and the
provider lessons (recurring-write invariants, conflict snapshots)
- docs/README.md — map of what documentation lives where, incl. the
convention that superpowers/ plans are historical artifacts while
.planning/ stays current
- README.md — showcase layout (centered header, badges, screenshot
gallery from the fastlane assets, grouped features, install/build/
architecture/roadmap sections); renders on Gitea
- .planning/{PROJECT,REQUIREMENTS,STATE}.md unstaled: read-only-V1 talk
removed, V1/V2 checklists marked shipped, state points at v3 + the
Locations & People go/no-go
release.yaml gains a gitea-release job: on every tag push it extracts the
tag's CHANGELOG section and creates a Gitea release with it as the notes.
No APK assets — distribution stays with the F-Droid repo. Idempotent
(skips an existing release), gated on the test job only so notes appear
even when the F-Droid upload hiccups.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- skip setup-android's default packages (pulled the ~300 MB emulator every run)
- drop unused platforms;android-36 and the dead jq install step
- cache /opt/android-sdk and ~/.gradle (release.yaml had no cache at all)
- drop --no-daemon so lint/test/assemble reuse one warm daemon per job
- Trivy scan only on main (advisory-only; was ~25s tax on every branch push)
- concurrency group cancels superseded runs; drop duplicate pull_request trigger
Companion to the act_runner fix on the CI host: job containers now join the
runner's network so the actions/cache server is reachable (saves previously
failed with reserveCache timeouts, so no cache was ever stored).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ci.yaml: ./gradlew lint -> lintDebug, test -> testDebugUnitTest.
Default lint task runs for BOTH debug and release variants which
doubles the scan work; AGP's lint catalog is identical between
variants for our scope so debug-only is sufficient. Same for test:
testDebugUnitTest avoids running release-variant test compilation.
- release.yaml: drop lint step from ci-sanity job. Lint is enforced
on every push to main via ci.yaml; by the time a tag exists at a
main commit, lint has already passed. Release-sanity keeps test +
assembleDebug to catch any tag-resolved drift (e.g. version code
substitution issues).
Expected CI run time reduction: ~30% (lint accounts for the largest
single block of cold-cache work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The release workflow's ci-sanity job ran 'lint test assembleDebug' as
a single gradle invocation, which combined all three phases in one
JVM and exceeded the 2GB heap inside the gitea-actions docker
container ("Gradle build daemon disappeared unexpectedly"). Split
into three separate invocations matching ci.yaml - each gradle call
gets its own fresh 2GB JVM, well under the container's memory ceiling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triggers on git tags. Runs CI sanity (lint+test+assembleDebug), then
in build-and-deploy job: writes version from tag into app/build.gradle.kts
(versionCode = MAJOR*10000 + MINOR*100 + PATCH, HouseHoldKeaper
convention), drops keystore + key.properties from secrets, runs
assembleRelease, pulls existing F-Droid repo from Hetzner, drops the
new APK + metadata, regenerates index with 'fdroid update -c', and
SCPs the whole tree back to Hetzner.
Required secrets: KEYSTORE_BASE64, KEY_PASSWORD, KEY_ALIAS,
HETZNER_HOST, HETZNER_USER, HETZNER_PASS. Configure these in Gitea
repo settings before pushing the first tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>