Move the canonical forge to Codeberg, and add Renovate + Weblate (#2)
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. Ports the setup Calendula already runs on, adapted where Agendula genuinely differs. **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 · 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. CI also gains three fixes: an explicit `SKIP_RE` skip-list that names the build-relevant files in the log, base-ref normalisation, and a fully-qualified `android-actions/setup-android` — Codeberg resolves bare `uses:` refs against `data.forgejo.org`, which does not carry that action. ### 2 · Three release-pipeline safety changes - `detect` and the Renovate job get 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 these 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. - **The Codeberg publish step pushes the tag itself** instead of waiting for it to arrive by mirror. That wait was correct while Gitea mirrored *to* Codeberg; under Codeberg-canonical the mirror runs the other way and it would never resolve. Attaching the release to an already-pushed ref (no `target_commitish`) is what avoids the empty-bodied 500s, and the create call retries with backoff because Codeberg 500s on a tag it has only just received. The step stays **fail-loud**, not `continue-on-error` — it reported green through 0.2.1–0.3.2 while never once publishing, and that must not be possible again. ### 3 · Renovate `renovate.json5` plus a Gitea-side job targeting Codeberg's API. `managerFilePatterns` covers **both** workflow directories, so the pinned Renovate image tag and the action versions in either file keep getting bumped. Needs two new Gitea secrets: `RENOVATE_TOKEN` (Codeberg bot, repo read/write + PR scope) and `GITHUB_COM_TOKEN` (read-only github.com PAT, for changelog lookups). ### 4 · Weblate A parity check (`scripts/check_translations.py`) runs on every PR without a path filter, so the required `Translations` status is always reported. Partial translations are expected, so `MissingTranslation` and `MissingQuantity` become informational — `ExtraTranslation` stays fatal. Agendula had no `lint` block at all, so the first locale to land would otherwise have failed the build. **Settings → App language** now opens a picker carrying a "Help translate" header. That is why it drops floret-kit's `LanguagePickerRow` for a local row: the shared recipe has no `header` slot, and the framing is app-specific rather than a family primitive. ### 5 · Links repointed In-app Source / License / report-issue URLs, F-Droid metadata, README (now with a Codeberg CI badge), and the docs. `floret-kit` follows suit — `.gitmodules` points at `codeberg.org/jlmakiola/floret-kit`, so a clone no longer needs to reach the personal Gitea instance to resolve it. The Gitea copy is **kept**: every existing tag records the old submodule URL, so rebuilds of past releases still resolve. ### 6 · Housekeeping Drops `release-notes.md` — a release-pipeline scratch file that got committed — and gitignores the five others the release job writes into the workspace. ### Not in this PR The Codeberg → Gitea push mirror, the Weblate component, and the Codeberg bot account (all browser-side). Until the mirror is flipped, merging this does **not** reach the Gitea runner. Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/2
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
# Agendula — releasing
|
||||
|
||||
Agendula is distributed through a **self-hosted F-Droid repo** (on Hetzner) with
|
||||
a human-readable **Gitea release** per version. Both are produced automatically
|
||||
by `.gitea/workflows/release.yaml` when a **bumped `versionName` reaches `main`**
|
||||
— the pipeline builds and publishes that version, then creates the matching
|
||||
`vX.Y.Z` tag and Gitea release itself. There are no APK assets on the Gitea
|
||||
release: distribution lives in the F-Droid repo; the release is the changelog of
|
||||
record.
|
||||
Agendula is distributed through a **self-hosted F-Droid repo** (on Hetzner) and a
|
||||
**Codeberg release** per version carrying the signed APK as a direct download.
|
||||
Both are produced automatically by `.gitea/workflows/release.yaml` when a
|
||||
**bumped `versionName` reaches `main`** — the pipeline builds and publishes that
|
||||
version, then creates the matching `vX.Y.Z` tag and the releases itself. The
|
||||
parallel **Gitea release** is the changelog of record on the build instance and
|
||||
carries no APK assets.
|
||||
|
||||
While Agendula is pre-1.0 (`versionName` starts with `0.`), every Gitea release
|
||||
is flagged as a **pre-release**. This happens automatically and graduates to a
|
||||
Codeberg is the canonical forge; Gitea is build infrastructure. See
|
||||
[Two forges, one repo](#two-forges-one-repo) for how the two are wired.
|
||||
|
||||
While Agendula is pre-1.0 (`versionName` starts with `0.`), every release is
|
||||
flagged as a **pre-release**. This happens automatically and graduates to a
|
||||
stable release at `1.0.0` — no manual toggling.
|
||||
|
||||
---
|
||||
@@ -80,35 +83,49 @@ re-running the workflow safely retries).
|
||||
CI and release are split so a change is built once on its PR and only does
|
||||
release work when a merge actually cuts a release:
|
||||
|
||||
- **`ci.yaml`** (on `pull_request`) — the reproducible-release invariant guard
|
||||
- **`ci.yaml`** (`.forgejo/workflows/`, on `pull_request`, **Codeberg**) — the
|
||||
reproducible-release invariant guard
|
||||
(`scripts/check_reproducible_release.sh`), then lint + unit tests + a debug
|
||||
assemble and a Trivy scan, once per PR. Docs/metadata-only PRs skip the Android
|
||||
build but still report a green `CI` check.
|
||||
- **`release.yaml`** (on push to `main`, plus `workflow_dispatch`) — a cheap
|
||||
`detect` job reads `versionName` and checks whether a tag for it already
|
||||
exists. Only when it doesn't does the `release` job run: unit tests on the
|
||||
merged commit, pin `versionCode`, build & sign the release APK with the **app
|
||||
key**, copy it into the F-Droid repo, generate the per-version changelog from
|
||||
the fastlane tree, re-sign the index with the **repo key**, upload `repo/` +
|
||||
`metadata/`, then create the `vX.Y.Z` tag + Gitea release (CHANGELOG section as
|
||||
notes, flagged pre-release while `MAJOR` is 0), attach the R8 `mapping.txt`,
|
||||
and mirror the release to **Codeberg** with the signed APK + a SHA-256 checksum
|
||||
(both best-effort). Ordinary merges with no version bump fall through `detect`
|
||||
and do nothing.
|
||||
- **`translations.yaml`** (`.forgejo/workflows/`, on `pull_request`, **Codeberg**)
|
||||
— an SDK-free parity check (`scripts/check_translations.py`) over
|
||||
`values-*/strings.xml`, so Weblate PRs get fast feedback. Runs on every PR
|
||||
without a path filter, so the required `Translations / check` status is always
|
||||
reported.
|
||||
- **`renovate.yml`** (`.gitea/workflows/`, Mondays 05:00 UTC plus
|
||||
`workflow_dispatch`, **Gitea**) — the dependency sweep. Runs the pinned
|
||||
Renovate image as its job container and targets Codeberg's API; the cadence
|
||||
lives here, not in `renovate.json5`, so the two don't double-gate.
|
||||
- **`release.yaml`** (`.gitea/workflows/`, on push to `main` plus
|
||||
`workflow_dispatch`, **Gitea**) — a cheap `detect` job reads `versionName` and
|
||||
checks **Codeberg** for a tag for it. Only when there isn't one does the
|
||||
`release` job run: unit tests on the merged commit, pin `versionCode`, build &
|
||||
sign the release APK with the **app key**, copy it into the F-Droid repo,
|
||||
generate the per-version changelog from the fastlane tree, re-sign the index
|
||||
with the **repo key**, upload `repo/` + `metadata/`, then create the `vX.Y.Z`
|
||||
tag + Gitea release (CHANGELOG section as notes, flagged pre-release while
|
||||
`MAJOR` is 0), attach the R8 `mapping.txt`, and publish the release on
|
||||
**Codeberg** with the signed APK + a SHA-256 checksum. Ordinary merges with no
|
||||
version bump fall through `detect` and do nothing.
|
||||
|
||||
### Codeberg direct-download channel
|
||||
|
||||
Alongside F-Droid, each release is mirrored to the Codeberg repo
|
||||
Alongside F-Droid, each release is published on the Codeberg repo
|
||||
(`jlmakiola/agendula`) as a plain download 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 `agendula_v<version>.apk` + its `.sha256`. It's the
|
||||
same APK the F-Droid repo serves (same **app key**), so it adds no trust surface.
|
||||
The step is best-effort: a Codeberg outage never fails an already-published
|
||||
F-Droid release, and it skips cleanly if `CODEBERG_RELEASE_TOKEN` is unset.
|
||||
Releases aren't git objects and don't sync with the push mirror in either
|
||||
direction, so the pipeline pushes the `vX.Y.Z` tag straight to Codeberg, creates
|
||||
the release over the Codeberg API, and attaches `agendula_v<version>.apk` + its
|
||||
`.sha256`. It's the same APK the F-Droid repo serves (same **app key**), so it
|
||||
adds no trust surface. It skips cleanly if `CODEBERG_RELEASE_TOKEN` is unset,
|
||||
but it is **not** `continue-on-error`: through 0.2.1–0.3.2 this step reported
|
||||
green while never once publishing, which is how a crash-fix release reached
|
||||
F-Droid but not the Codeberg/Obtainium users who needed it. A broken mirror
|
||||
fails the release loudly.
|
||||
|
||||
One-time setup: the Codeberg repo's **Releases** unit must be enabled and a
|
||||
`CODEBERG_RELEASE_TOKEN` secret (Codeberg access token, `write:repository` scope)
|
||||
added to Gitea Actions.
|
||||
`CODEBERG_RELEASE_TOKEN` secret (Codeberg access token, `write:repository` scope
|
||||
— it pushes the tag as well as creating the release) added to Gitea Actions.
|
||||
|
||||
### Manual re-sign / recovery
|
||||
|
||||
@@ -120,6 +137,51 @@ rotation or repo recovery without publishing a new app version.
|
||||
|
||||
---
|
||||
|
||||
## Two forges, one repo
|
||||
|
||||
**Codeberg (`jlmakiola/agendula`) is canonical** — git, issues, PRs, tags and
|
||||
releases. The self-hosted Gitea instance is build infrastructure: it holds the
|
||||
signing key, publishes the F-Droid repo, and runs the release pipeline. Codeberg
|
||||
push-mirrors `main` and tags to Gitea, and a bumped `versionName` arriving there
|
||||
triggers `release.yaml` exactly as before.
|
||||
|
||||
Workflows are separated by **directory**, not by conditionals. Forgejo looks in
|
||||
`.forgejo/workflows` → `.gitea/workflows` → `.github/workflows` and stops at the
|
||||
first that exists; Gitea doesn't know `.forgejo/` at all:
|
||||
|
||||
| Directory | Runs on | Contains | Secrets |
|
||||
| --- | --- | --- | --- |
|
||||
| `.forgejo/workflows/` | Codeberg | `ci.yaml`, `translations.yaml` | **none** |
|
||||
| `.gitea/workflows/` | Gitea | `release.yaml`, `renovate.yml` | signing key, F-Droid, Hetzner, bot tokens |
|
||||
|
||||
The line is drawn at **secrets, not at CI-vs-release**. That's what makes fork
|
||||
PRs safe: everything a contributor can trigger lives in `.forgejo/` and can
|
||||
reference no secret. Renovate stays on the Gitea runner *even though it opens
|
||||
PRs on Codeberg* — it talks to Codeberg's API rather than moving its token onto
|
||||
the contributor-facing runner. `detect` (and the Renovate job) additionally
|
||||
carries a
|
||||
`github.repository_owner == 'makiolaj'` guard, because the directory split only
|
||||
holds while `.forgejo/` is non-empty — empty it and Codeberg would fall back to
|
||||
`.gitea/` and run the release pipeline on the contributor-facing runner.
|
||||
|
||||
Two consequences worth remembering:
|
||||
|
||||
- **`detect` 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 would re-cut a shipped release.
|
||||
- **Any ref that exists only on Gitea gets deleted** by the mirror. That's
|
||||
correct under Codeberg-canonical, but don't debug a "vanished" branch without
|
||||
remembering it.
|
||||
|
||||
`floret-kit` is a submodule of this repo and follows the same move: `.gitmodules`
|
||||
points at `https://codeberg.org/jlmakiola/floret-kit.git`, so a clone resolves
|
||||
without reaching the personal Gitea instance. The Gitea copy is **kept** — every
|
||||
existing tag records the old submodule URL, so rebuilds of past releases still
|
||||
resolve.
|
||||
|
||||
---
|
||||
|
||||
## Secrets (Gitea → repo Settings → Actions → Secrets)
|
||||
|
||||
The workflow fails loudly if the F-Droid ones are missing — it will **never**
|
||||
@@ -133,7 +195,9 @@ user's pinned repo).
|
||||
| `FDROID_CONFIG_BASE64` | F-Droid `config.yml` (base64) — repo metadata + keystore passwords. |
|
||||
| `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS` | Upload target for the F-Droid repo. |
|
||||
| `GITHUB_TOKEN` | Provided by Gitea Actions; used to create the release + attach assets. |
|
||||
| `CODEBERG_RELEASE_TOKEN` | Codeberg access token (`write:repository` scope) — creates the mirrored Codeberg release + uploads the APK/checksum. Best-effort; if unset the Codeberg step skips. |
|
||||
| `CODEBERG_RELEASE_TOKEN` | Codeberg access token (`write:repository` scope) — pushes the tag to Codeberg, creates the release there and uploads the APK/checksum. If unset the step skips; if set and failing, the release fails. |
|
||||
| `RENOVATE_TOKEN` | Codeberg bot-account token — repo read/write + PR scope on `jlmakiola/agendula`. Used only by `renovate.yml`. |
|
||||
| `GITHUB_COM_TOKEN` | Read-only github.com PAT (no scopes). Without it Renovate's changelog lookups hit the 60/h anonymous rate limit and PRs arrive with empty release notes. |
|
||||
|
||||
The app key signs APKs; the repo key signs the index (its fingerprint is what
|
||||
users pin). Neither key nor `config.yml` is ever uploaded to the server — they
|
||||
|
||||
Reference in New Issue
Block a user