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
Three gaps in the Renovate setup, all about having enough in front of you at review time.
### Age reads UNKNOWN
`config:recommended` already brings in `mergeConfidence:age-confidence-badges`, so the Age column existed — it just rendered grey. Fetching the badge SVGs from `developer.mend.io` directly shows why: Mend's index covers Maven Central but has nothing for Google's Maven repo, so every androidx/compose coordinate is blank. No token changes it; the JSON API behind the badges answers 401 for everyone.
| coordinate | registry | age badge |
| --- | --- | --- |
| `androidx.compose:compose-bom` | Google Maven | UNKNOWN |
| `androidx.core:core-ktx` | Google Maven | UNKNOWN |
| `com.google.truth:truth` | Maven Central | 2y |
| `org.jetbrains.kotlin:kotlin-stdlib` | Maven Central | 1y (confidence high, passing 99%) |
Age is now computed from `newVersionAgeInDays`, which Renovate derives itself to evaluate `minimumReleaseAge` — Google Maven serves `last-modified` on its POMs, so it's populated where Mend is blank, and the number agrees with the tiers it's read against. Mend keeps the Confidence column, which still resolves for the Maven Central half (Kotlin, Gradle, AGP, the test stack).
### Empty release notes
We run against Gitea, but the packages are *released* on GitHub, so changelog lookups were going out unauthenticated against a 60/h limit. `RENOVATE_GITHUB_COM_TOKEN` lifts that.
**Needs a secret before it does anything:** a github.com PAT with **no scopes ticked**, added as repo secret `GITHUB_COM_TOKEN`. Until then the var resolves empty, which is exactly today's behaviour.
### Nothing expressed how settled a release is
Cooling-off scaled by blast radius: 30 days major, 20 minor, 10 patch/digest.
Deliberately advisory. Renovate's default `internalChecksFilter: strict` would suppress the PR outright until the version aged in; `none` opens it at the highest version immediately, so merging ahead of the window stays a decision rather than a wait. A too-young release still gets a yellow `renovate/stability-days` check — `setStability` computes that from `minimumReleaseAge` + `releaseTimestamp` independently of the filter — and with `automerge: false` nothing acts on it.
### Notes
Validated with `renovate-config-validator` against the pinned 43.232.0. Config is read from the default branch, so the open bump PRs keep their current tables until the next run after this merges.
Reviewed-on: #98
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>
The release/* branch protection requires the "Translations / check" status,
but the workflow was path-filtered to translation resources. A code-only PR
targeting a release branch never touches those, so the workflow never ran,
never posted its status, and the required check stayed pending forever —
permanently blocking the merge (only PRs that happened to change strings could
satisfy it).
Drop the path filter so it runs on every PR, mirroring the always-on `ci` job.
The parity check is SDK-free and passes when the committed translations are
consistent, so running it on unrelated PRs is effectively free.
Co-Authored-By: Claude Opus 4.8 <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>
v2.7.3 is the first release built with `vcsInfo { include = false }` and is
verified reproducible: a clean from-source build matched the published
calendula_v2.7.3.apk byte-for-byte across all 1382 zip entries (only the
signature block differs, which F-Droid copies).
- Point the fdroiddata recipe's Builds entry + CurrentVersion at v2.7.3
(was the 2.7.2 placeholder, which lacks the fix and would fail verification).
- UpdateCheckMode: Tags ^v[0-9.]+$ — after this one-time submission F-Droid
auto-tracks new release tags and adds build entries itself; no manual recipe
edits per release.
- Add scripts/check_reproducible_release.sh + an always-on CI step asserting the
release build keeps VCS-info disabled, so reproducibility can't silently
regress and quietly stop official-repo publishing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The scope step shallow-fetched the base (`--depth=1`) and ran
`git diff origin/<base>...HEAD`. The three-dot diff needs the merge-base,
which a depth-1 tip doesn't contain once a branch has forked a few commits
back — git aborts with "no merge base" (exit 128) and the whole CI job
fails before lint/test/build ever run.
Fetch the base fully, resolve the merge-base explicitly, and diff from it.
If no common ancestor is found, default to code=true so the build still
runs rather than being wrongly skipped.
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>
renovatebot/github-action is a Node wrapper that shells out to
`docker run ghcr.io/renovatebot/renovate`, requiring a Docker CLI + socket
inside the job. The Gitea runner executes the job in a plain node:22 container
with neither, so it died on "Unable to locate executable file: docker".
Run the renovate image as the job container and invoke `renovate` directly —
drops the docker-in-docker requirement. Full tag pinned; Renovate's
github-actions manager keeps container.image bumped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renovatebot/github-action ships only full semver tags; @v40 was an
invalid ref and the dispatched run failed to resolve it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renovate.json5 (config:recommended + semantic commits, no automerge,
dependency dashboard; material3 stays on its 1.5-alpha pin in an
isolated PR; test deps grouped; github-actions manager watches
.gitea/workflows). Cadence owned by .gitea/workflows/renovate.yml
(Mondays 05:00 UTC + manual dispatch), self-hosted via
renovatebot/github-action, scoped to makiolaj/calendula.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add scripts/check_translations.py and a lightweight Translations workflow
that runs it (no Android SDK needed) so Weblate PRs get fast feedback. The
script fails on stale keys (present in a translation but not the base) and on
translating translatable="false" entries; missing keys are reported as
coverage only.
Downgrade lint's MissingTranslation to informational: partial community
translations are expected and fall back to the English base at runtime.
Stale/extra keys (ExtraTranslation) remain fatal in lintDebug.
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>
Runs on every push to any branch (tags excluded) and on pull requests.
Installs JDK 17 + Android SDK 36 + 37.0-preview (needed because the
Material 3 Expressive alpha transitively requires compileSdk 37).
Gradle dependency cache keyed on libs.versions.toml. Trivy scan runs
with continue-on-error like HouseHoldKeaper - we report findings but
don't block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>