From db7094c54e328bb03d549c92c4f44eb4374e4155 Mon Sep 17 00:00:00 2001
From: Jean-Luc Makiola
Date: Wed, 29 Jul 2026 20:33:56 +0200
Subject: [PATCH] Move the canonical forge to Codeberg (#86)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/86
---
{.gitea => .forgejo}/workflows/ci.yaml | 7 ++-
.../workflows/translations.yaml | 0
.gitea/workflows/release.yaml | 51 +++++++++++++++----
.gitea/workflows/renovate.yml | 27 +++++++---
.gitmodules | 2 +-
CHANGELOG.md | 7 +++
README.md | 2 +-
app/src/main/res/values/strings.xml | 4 +-
docs/RELEASING.md | 42 +++++++++++++--
.../de.jeanlucmakiola.calendula.yml | 6 +--
.../de.jeanlucmakiola.calendula.yml | 4 +-
11 files changed, 119 insertions(+), 33 deletions(-)
rename {.gitea => .forgejo}/workflows/ci.yaml (92%)
rename {.gitea => .forgejo}/workflows/translations.yaml (100%)
diff --git a/.gitea/workflows/ci.yaml b/.forgejo/workflows/ci.yaml
similarity index 92%
rename from .gitea/workflows/ci.yaml
rename to .forgejo/workflows/ci.yaml
index 9f15571..65831ab 100644
--- a/.gitea/workflows/ci.yaml
+++ b/.forgejo/workflows/ci.yaml
@@ -72,9 +72,14 @@ jobs:
distribution: 'zulu'
java-version: '17'
+ # Fully qualified on purpose. Codeberg resolves bare `uses:` refs against
+ # data.forgejo.org, Forgejo's own action mirror — actions/checkout,
+ # setup-java and cache all exist there, but android-actions/setup-android
+ # does not, and the job dies with "repository not found". Gitea's instance
+ # defaults to GitHub, which is why this never surfaced before the split.
- name: Setup Android SDK
if: steps.scope.outputs.code == 'true'
- uses: android-actions/setup-android@v3
+ uses: https://github.com/android-actions/setup-android@v3
with:
# Default ("tools platform-tools") drags in the Android Emulator
# (~300 MB) which the build never uses.
diff --git a/.gitea/workflows/translations.yaml b/.forgejo/workflows/translations.yaml
similarity index 100%
rename from .gitea/workflows/translations.yaml
rename to .forgejo/workflows/translations.yaml
diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml
index bf0f83e..17a4efd 100644
--- a/.gitea/workflows/release.yaml
+++ b/.gitea/workflows/release.yaml
@@ -27,6 +27,14 @@ jobs:
# whether this push actually cuts a new release (no tag for it yet). Keeps the
# heavy job from running on every merge to main.
detect:
+ # Gitea only. The workflow directory split already keeps this file invisible
+ # to Codeberg — Forgejo's lookup is first-match-wins, and .forgejo/workflows
+ # exists — but that only holds while .forgejo/ is non-empty. Move the last
+ # file out of it and Codeberg would fall back to .gitea/workflows and start
+ # running the release pipeline on the contributor-facing runner, with no
+ # secrets. repository_owner differs between the two forges regardless of
+ # URL, proxy or instance rename, so this closes it permanently.
+ if: github.repository_owner == 'makiolaj'
runs-on: docker
outputs:
is_release: ${{ steps.v.outputs.is_release }}
@@ -41,8 +49,16 @@ jobs:
- name: Resolve version and whether it is a new release
id: v
env:
- TOKEN: ${{ secrets.GITHUB_TOKEN }}
- API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
+ # Tags are read from Codeberg, which is canonical — deliberately NOT
+ # from the Gitea API this workflow runs on. The Codeberg -> Gitea sync
+ # is a push mirror, i.e. `git push --mirror`, which deletes refs the
+ # source does not have. A tag minted here on Gitea is therefore wiped
+ # by the next sync (Codeberg does not have it yet) and only reappears
+ # once the tag push at the end of this workflow propagates back.
+ # Asking Gitea inside that window would report "no tag" for a release
+ # that already shipped, and cut it a second time.
+ # Public repo, so this read needs no token.
+ TAG_API: https://codeberg.org/api/v1/repos/jlmakiola/calendula
run: |
set -e
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
@@ -60,15 +76,28 @@ jobs:
fi
# A tag for this version already existing means the release shipped on
# an earlier push; do nothing. Absent => this merge cuts the release.
- STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
- -H "Authorization: token $TOKEN" "$API/git/refs/tags/v$VERSION")
- if [ "$STATUS" = "200" ]; then
- echo "Tag v$VERSION already exists — nothing to release."
- echo "is_release=false" >> "$GITHUB_OUTPUT"
- else
- echo "No tag for v$VERSION yet — cutting the release."
- echo "is_release=true" >> "$GITHUB_OUTPUT"
- fi
+ #
+ # Anything other than a clean 200/404 is treated as fatal rather than
+ # as "no tag". A Codeberg outage or a network blip would otherwise
+ # read as absent and re-cut a release that has already shipped —
+ # republishing to F-Droid and Play. Failing here is recoverable; a
+ # duplicate release is not.
+ STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$TAG_API/git/refs/tags/v$VERSION" || echo 000)
+ case "$STATUS" in
+ 200)
+ echo "Tag v$VERSION already exists on Codeberg — nothing to release."
+ echo "is_release=false" >> "$GITHUB_OUTPUT"
+ ;;
+ 404)
+ echo "No tag for v$VERSION on Codeberg yet — cutting the release."
+ echo "is_release=true" >> "$GITHUB_OUTPUT"
+ ;;
+ *)
+ echo "Codeberg tag lookup for v$VERSION returned HTTP $STATUS." >&2
+ echo "Refusing to guess: treating this as 'no tag' could re-cut a shipped release." >&2
+ exit 1
+ ;;
+ esac
# Releases: build + sign + publish, then mint the tag and Gitea release.
# Also runs on manual dispatch, where it skips the build and just re-signs and
diff --git a/.gitea/workflows/renovate.yml b/.gitea/workflows/renovate.yml
index 4a39197..53f7a53 100644
--- a/.gitea/workflows/renovate.yml
+++ b/.gitea/workflows/renovate.yml
@@ -29,18 +29,29 @@ jobs:
- name: Run Renovate
run: renovate
env:
- # Self-hosted Gitea, not github.com.
- RENOVATE_PLATFORM: gitea
- RENOVATE_ENDPOINT: https://gitea.jeanlucmakiola.de/api/v1
- # Bot-account token (Gitea secret). Needs repo read/write + PR scope.
+ # Renovate targets Codeberg (canonical) while still RUNNING on the
+ # Gitea runner. Moving the job to Codeberg would put a repo-write
+ # token on the contributor-facing runner, which is exactly what the
+ # .forgejo/ vs .gitea/ split exists to prevent — so the token stays
+ # where the other secrets live and only the API calls cross over.
+ #
+ # Platform is `forgejo`, not `gitea`: Codeberg runs Forgejo, and the
+ # pinned image ships a distinct forgejo platform module.
+ RENOVATE_PLATFORM: forgejo
+ RENOVATE_ENDPOINT: https://codeberg.org/api/v1
+ # Codeberg bot-account token (Gitea secret). Needs repo read/write +
+ # PR scope on jlmakiola/calendula.
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
# Scope to this repo only — no org-wide autodiscovery.
RENOVATE_AUTODISCOVER: 'false'
- RENOVATE_REPOSITORIES: '["makiolaj/calendula"]'
- # Commits/PRs authored as the bot, not a real maintainer.
+ RENOVATE_REPOSITORIES: '["jlmakiola/calendula"]'
+ # Commits/PRs authored as the bot, not a real maintainer. This address
+ # must be a verified email on the Codeberg bot account, otherwise the
+ # commits show up unattributed there.
RENOVATE_GIT_AUTHOR: 'Renovate Bot '
- # Read-only github.com PAT (no scopes needed). We run on Gitea, but
- # nearly every dependency is *released* on GitHub — without this,
+ # Read-only github.com PAT (no scopes needed). Unaffected by the forge
+ # move — nearly every dependency is *released* on GitHub, and without
+ # this,
# changelog/release-note lookups hit the 60/h anonymous rate limit
# and PRs arrive with an empty "Release Notes" section.
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.GITHUB_COM_TOKEN }}
diff --git a/.gitmodules b/.gitmodules
index 103c066..b908ca7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "floret-kit"]
path = floret-kit
- url = https://gitea.jeanlucmakiola.de/makiolaj/floret-kit.git
+ url = https://codeberg.org/jlmakiola/floret-kit.git
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c16f6ae..8fa6eb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+- Calendula's source code now lives on **Codeberg**, where its issues already
+ were. The **Source code** and **License** links in Settings → About point
+ there, so reporting a bug and reading the code no longer land on two different
+ sites. Nothing about the app itself changes, and the F-Droid repository is
+ unaffected.
+
## [2.16.0] — 2026-07-24
### Added
diff --git a/README.md b/README.md
index f909377..f2e3b31 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Reads, writes, and reminds — on top of the system calendar, with zero network access.
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 99407b9..79fdb43 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -571,8 +571,8 @@
Add Quick Settings tile
Add a “New event” tile to the Quick Settings panel.
- https://gitea.jeanlucmakiola.de/makiolaj/calendula
- https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/LICENSE
+ https://codeberg.org/jlmakiola/calendula
+ https://codeberg.org/jlmakiola/calendula/src/branch/main/LICENSE
https://jeanlucmakiola.de/calendula/privacy
https://ko-fi.com/jeanlucmakiola
https://weblate.dev.jeanlucmakiola.de/engage/calendula/
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index 4bd4f03..4426944 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -73,7 +73,8 @@ Published version codes so far: `v0.1.0`→100 … `v1.0.0`→10000 … `v2.0.0`
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`) — lint + unit tests + a debug assemble (and
+- **`ci.yaml`** (`.forgejo/workflows/`, on `pull_request`, **Codeberg**) — 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
@@ -91,9 +92,9 @@ release work when a merge actually cuts a release:
Alongside F-Droid, each release is mirrored to the Codeberg repo
(`jlmakiola/calendula`) 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 `calendula_v.apk` + its `.sha256`. It's the
+Codeberg **push-mirrors** branches and tags to Gitea, but releases aren't git
+objects and don't sync in either direction, so the pipeline creates the release
+over the Codeberg API and attaches `calendula_v.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
@@ -109,6 +110,39 @@ build, the version bump, and tag/release creation, and just re-signs the
existing F-Droid index with the configured repo key and re-uploads. Use this
for key rotation or repo recovery without publishing a new app version.
+## Two forges, one repo
+
+**Codeberg (`jlmakiola/calendula`) 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, Play, 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.
+
+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.
+
## Secrets (Gitea → repo Settings → Actions → Secrets)
| Secret | Purpose |
diff --git a/docs/fdroid-official/de.jeanlucmakiola.calendula.yml b/docs/fdroid-official/de.jeanlucmakiola.calendula.yml
index 86b95a7..aa9c737 100644
--- a/docs/fdroid-official/de.jeanlucmakiola.calendula.yml
+++ b/docs/fdroid-official/de.jeanlucmakiola.calendula.yml
@@ -18,9 +18,9 @@ Categories:
- Calendar & Agenda
License: MIT
AuthorName: Jean-Luc Makiola
-SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
-IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
-Changelog: https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/CHANGELOG.md
+SourceCode: https://codeberg.org/jlmakiola/calendula
+IssueTracker: https://codeberg.org/jlmakiola/calendula/issues
+Changelog: https://codeberg.org/jlmakiola/calendula/src/branch/main/CHANGELOG.md
Donate: https://ko-fi.com/jeanlucmakiola
AutoName: Calendula
diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula.yml b/fdroid-metadata/de.jeanlucmakiola.calendula.yml
index 2f6581e..61c667b 100644
--- a/fdroid-metadata/de.jeanlucmakiola.calendula.yml
+++ b/fdroid-metadata/de.jeanlucmakiola.calendula.yml
@@ -6,6 +6,6 @@ Summary: A modern Material 3 Expressive calendar for Android.
Categories:
- Time
-SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula
-IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues
+SourceCode: https://codeberg.org/jlmakiola/calendula
+IssueTracker: https://codeberg.org/jlmakiola/calendula/issues
Donate: https://ko-fi.com/jeanlucmakiola