From c6e83fc071f5ca6355976d379a88396377ac7a4f Mon Sep 17 00:00:00 2001
From: Jean-Luc Makiola
Date: Sat, 25 Jul 2026 20:09:22 +0000
Subject: [PATCH 1/5] chore(renovate): show real release age and fetch
changelogs (!98)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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: https://gitea.jeanlucmakiola.de/makiolaj/calendula/pulls/98
---
.gitea/workflows/renovate.yml | 5 ++++
renovate.json5 | 52 +++++++++++++++++++++++++++++++++++
2 files changed, 57 insertions(+)
diff --git a/.gitea/workflows/renovate.yml b/.gitea/workflows/renovate.yml
index 5b5ebda..4a39197 100644
--- a/.gitea/workflows/renovate.yml
+++ b/.gitea/workflows/renovate.yml
@@ -39,4 +39,9 @@ jobs:
RENOVATE_REPOSITORIES: '["makiolaj/calendula"]'
# Commits/PRs authored as the bot, not a real maintainer.
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,
+ # 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 }}
LOG_LEVEL: info
diff --git a/renovate.json5 b/renovate.json5
index 103bd3d..efc41ff 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -7,6 +7,23 @@
":semanticCommits",
],
+ // `config:recommended` brings in mergeConfidence:age-confidence-badges, whose
+ // Age column is a Mend badge. Mend's Merge Confidence index only covers Maven
+ // Central: org.jetbrains.kotlin, junit, truth, turbine et al resolve, but
+ // every androidx/compose artifact lives on Google's Maven repo and comes back
+ // as a grey UNKNOWN — i.e. most of this project. Renovate already knows the
+ // real answer, since it derives release timestamps itself for the
+ // minimumReleaseAge rules below (Google Maven serves `last-modified` on its
+ // POMs), so take the age from there and leave Mend to the Confidence column,
+ // which still carries signal for the Maven Central half.
+ prBodyDefinitions: {
+ Age: "{{#if releaseTimestamp}}{{{newVersionAgeInDays}}} d{{else}}unknown{{/if}}",
+ },
+ // Default heading links to the Merge Confidence docs; this column is ours now.
+ prBodyHeadingDefinitions: {
+ Age: "Age",
+ },
+
// No automerge: a dependency bump goes through the same review (and, for
// anything touching the build, the same on-device check) as a feature
// before it can ride a release — see docs/RELEASING.md and the
@@ -15,6 +32,16 @@
// One reviewable surface; the dashboard issue lists everything pending.
dependencyDashboard: true,
+
+ // The cooling-off periods below are advisory, not a gate: "none" turns off
+ // filtering on the minimumReleaseAge check, so the PR is opened at the
+ // highest version straight away and merging early stays a judgement call.
+ // (Renovate's default here is "strict", which suppresses the PR entirely
+ // until the release has aged in.) A still-young release carries a pending
+ // `renovate/stability-days` check so it's visible which side of the line
+ // it's on; with automerge off, nothing acts on that check by itself.
+ internalChecksFilter: "none",
+
labels: ["dependencies"],
prConcurrentLimit: 5,
prHourlyLimit: 0,
@@ -30,6 +57,21 @@
},
packageRules: [
+ // Cooling-off period, scaled by blast radius: how long a release should
+ // have been out (and un-yanked, un-hotfixed) before it's considered
+ // settled. Advisory only — see `internalChecksFilter` above.
+ {
+ matchUpdateTypes: ["major"],
+ minimumReleaseAge: "30 days",
+ },
+ {
+ matchUpdateTypes: ["minor"],
+ minimumReleaseAge: "20 days",
+ },
+ {
+ matchUpdateTypes: ["patch", "digest", "pin", "rollback"],
+ minimumReleaseAge: "10 days",
+ },
// material3 is deliberately pinned to the 1.5 *alpha* line for the
// Expressive APIs (see gradle/libs.versions.toml). Follow the alpha train
// but keep it in its own PR, reviewed in isolation; revisit the pin when
@@ -52,5 +94,15 @@
],
groupName: "test dependencies",
},
+ // Last word on the PR table. The merge-confidence preset sets prBodyColumns
+ // from inside a packageRule of its own, and only for the datasources Mend
+ // supports — so a plain top-level prBodyColumns would lose to it for maven
+ // deps, and the Gradle wrapper / Actions / container bumps would keep the
+ // default columns and show no age at all. A rule declared after it wins,
+ // and gives every PR the same table.
+ {
+ matchPackageNames: ["*"],
+ prBodyColumns: ["Package", "Type", "Change", "Age", "Confidence"],
+ },
],
}
From 314236ac0cd0ce09b31a1ec2b3b15effd0615f54 Mon Sep 17 00:00:00 2001
From: Jean-Luc Makiola
Date: Sat, 25 Jul 2026 20:31:33 +0000
Subject: [PATCH 2/5] fix(renovate): use a flexible internal-checks filter so
ages resolve (!100)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Age came back `unknown` for every dependency after #98. That's the `internalChecksFilter: "none"` from that PR doing what it says on the tin — and rather more.
### Why
`lib/workers/repository/process/lookup/filter-checks.ts` short-circuits on `none`:
```js
if (internalChecksFilter === 'none') {
release = sortedReleases.pop(); // returns here
} else {
for (let candidateRelease of sortedReleases.reverse()) {
...
const updatedCandidateRelease = await postprocessRelease(...)
```
`postprocessRelease` is the only caller that fetches a Maven artifact's `Last-Modified` header (`lib/modules/datasource/maven/index.ts:222`), so skipping the loop leaves `releaseTimestamp` unset. That empties the Age column — but it also silently voids `minimumReleaseAge` and the `renovate/stability-days` check, since both compare against that same timestamp. The cooling-off tiers were decorative.
### Fix
`flexible` keeps the intent — still prefers a version that has cleared its window, but opens the PR at the newest candidate when none has — while running the loop that populates the timestamps.
Verified with a local `--platform=local --dry-run=full`:
| dependency | new version | age |
| --- | --- | --- |
| `androidx.compose:compose-bom` | 2026.06.01 | 24 d |
| `androidx.compose.material3:material3` | 1.5.0-alpha24 | 10 d |
| `androidx.work:work-runtime-ktx` | 2.11.2 | 122 d |
| `com.android.application` (AGP) | 9.3.1 | 2 d |
| `gradle` | 9.6.1 | 29 d |
Only `ghcr.io/renovatebot/renovate` stays unknown, because the docker registry serves no timestamps at all — the run marks all 176 of its tags pending for that reason, and `flexible` is what still lets that PR through.
`Pending` rejoins the table: under a flexible filter it names the newer version being held back, rather than leaving it invisible.
### Also
Migrates the Gitea workflow manager off the deprecated `fileMatch`. Renovate's config migration was rewriting it to `managerFilePatterns` (delimited regex) on every run and warning about it; the dry run confirms the manager still picks up `.gitea/workflows` afterwards.
Validated with `renovate-config-validator` against the pinned 43.232.0.
Reviewed-on: https://gitea.jeanlucmakiola.de/makiolaj/calendula/pulls/100
---
renovate.json5 | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/renovate.json5 b/renovate.json5
index efc41ff..378bf07 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -33,14 +33,20 @@
// One reviewable surface; the dashboard issue lists everything pending.
dependencyDashboard: true,
- // The cooling-off periods below are advisory, not a gate: "none" turns off
- // filtering on the minimumReleaseAge check, so the PR is opened at the
- // highest version straight away and merging early stays a judgement call.
- // (Renovate's default here is "strict", which suppresses the PR entirely
- // until the release has aged in.) A still-young release carries a pending
+ // The cooling-off periods below are advisory, not a gate. "flexible" still
+ // prefers a version that has cleared its window, but when every candidate is
+ // too young it opens the PR at the newest one anyway, so merging early stays
+ // a judgement call. (The default, "strict", would suppress the PR entirely
+ // until a release aged in.) A still-young branch carries a yellow
// `renovate/stability-days` check so it's visible which side of the line
// it's on; with automerge off, nothing acts on that check by itself.
- internalChecksFilter: "none",
+ //
+ // NOT "none": that short-circuits the candidate loop in filter-checks.ts, and
+ // that loop is what calls postprocessRelease — the only thing that fetches a
+ // Maven artifact's Last-Modified header. Skipping it leaves releaseTimestamp
+ // unset, which empties the Age column and quietly makes minimumReleaseAge and
+ // the stability check no-ops, since both need that timestamp to compare.
+ internalChecksFilter: "flexible",
labels: ["dependencies"],
prConcurrentLimit: 5,
@@ -52,8 +58,10 @@
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
// the github-actions manager (same syntax) to watch them too.
+ // `fileMatch` is deprecated; the replacement takes the regex delimited, and
+ // Renovate's config migration was already rewriting this on every run.
"github-actions": {
- fileMatch: ["^\\.gitea/workflows/[^/]+\\.ya?ml$"],
+ managerFilePatterns: ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"],
},
packageRules: [
@@ -100,9 +108,12 @@
// deps, and the Gradle wrapper / Actions / container bumps would keep the
// default columns and show no age at all. A rule declared after it wins,
// and gives every PR the same table.
+ // "Pending" earns its place under a flexible filter: when the bump lands on
+ // a version that has cleared its window but a newer one hasn't, that newer
+ // version is named here rather than silently withheld.
{
matchPackageNames: ["*"],
- prBodyColumns: ["Package", "Type", "Change", "Age", "Confidence"],
+ prBodyColumns: ["Package", "Type", "Change", "Age", "Pending", "Confidence"],
},
],
}
From ac0c43f9308eff02740f4d2daac224c91d86feb6 Mon Sep 17 00:00:00 2001
From: Weblate
Date: Sun, 26 Jul 2026 08:26:54 +0000
Subject: [PATCH 3/5] Translations update from Weblate (!99)
Translations update from [Weblate](https://weblate.dev.jeanlucmakiola.de) for [Calendula/Strings](https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/).
Current translation status:

Co-authored-by: Bazyli Cyran
Co-authored-by: Sup99999
Reviewed-on: https://gitea.jeanlucmakiola.de/makiolaj/calendula/pulls/99
Co-authored-by: Weblate
Co-committed-by: Weblate
---
app/src/main/res/values-ar/strings.xml | 72 ++++++++++++++++++++++----
app/src/main/res/values-pl/strings.xml | 30 ++++++++++-
2 files changed, 92 insertions(+), 10 deletions(-)
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index ce0c971..49c17a2 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -53,11 +53,11 @@
تعذّر مشاركة هذا الحدث.
حذف الحدث؟
الحدث أُزيل من تقويمك ومن كل جهاز تتم مزامنته معه.
- حذف الحدث المتكرر
+ حذف الحدث المتكرّر
فقط هذا الحدث
هذا وجميع الأحداث اللاحقة
جميع الأحداث في السلسلة
- تعديل الحدث المتكرر
+ تعديل الحدث المتكرّر
تعذّر حذف الحدث
Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث
إلغاء
@@ -108,7 +108,7 @@
الحدث يبقى كما هو الآن
هل تريد تطبيق تذكيرك الافتراضي؟
هذا الحدث حُذف في هذه الأثناء، على سبيل المثال على جهاز آخر. لم يعد من الممكن حفظ تغييراتك.
- المزيد من الخيارات
+ المزيد من الخانات
علني
الافتراضي
مشغول
@@ -153,7 +153,7 @@
%1$s حتى %2$s
%1$s، %2$d مرّات
طَبِّق الافتراضي
- حدث مُتكرر
+ حدث متكرّر
%1$s في %2$s
—
تم القُبول
@@ -166,7 +166,7 @@
ملغي
متاح
خاص
- سري
+ سرّي
المُنظِّم
اختياري
التذكير الافتراضي
@@ -180,7 +180,7 @@
Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.
كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.
هل تستخدم تطبيق تقويم ثانٍ؟
- إذا كان تطبيق آخر أيضًا ينشر تذكيرات، فستراهم مرتين — قم بإيقاف تشغيلهم هناك أو هنا.
+ إذا تطبيق آخر أيضًا ينشر تذكيرات، فستراهم مرتين — قم بإيقاف تشغيلهم هناك أو هنا.
يمكنك تغييره في أي وقت
شَغِّل التذكيرات
ليس الآن
@@ -200,7 +200,7 @@
البحث عن الأحداث
مسح
لا مزيد من الأحداث اليوم
- لا توجد أحداث مطابقة ”%1$s“.
+ لا أحداث مطابقة ”%1$s“.
ابحث عن أحداثك بالعنوان، الموقع أو الملاحظات.
الرجوع
البحث
@@ -217,7 +217,7 @@
رجوع
المظهر
اللون الديناميكي
- يتطلب Android 12 أو أحدث
+ يتطلب أندرويد ١٢ أو أحدث
طريقة العرض الافتراضية
ألوان تقويم ناعمة
خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.
@@ -227,7 +227,7 @@
اختر ملفًا…
خط مخصص
تعذّر قراءة هذا الملف كخط
- يبدأ الأسبوع في
+ الأسبوع يبدأ في
تلقائي
تلقائي
تنسيق الوقت
@@ -243,4 +243,58 @@
إظهار
إخفاء
جدول
+ المنطقة الزمنية للجهاز
+ البحث عن المناطق الزمنية
+ جميع المناطق الزمنية
+ لا منطقة زمنية تطابق \"%1$s\"
+ %1$s توقيتك
+ الأحدث
+ أدخل رقمًا من ١ إلى ٩٩٩
+
+ - (%d) لا دقائق
+ - (%d) دقيقة واحده
+ - %d دقيقتان
+ - %d دقائق
+ - %d دقيقة
+ - %d دقيقة
+
+
+ - (%d) لا ساعات
+ - (%d) ساعة واحده
+ - %d ساعتان
+ - %d ساعات
+ - %d ساعة
+ - %d ساعة
+
+
+ - (%d) لا أيام
+ - (%d) يوم واحد
+ - %d يومان
+ - %d أيام
+ - %d يوم
+ - %d يوم
+
+
+ - (%d) لا أسابيع
+ - (%d) أسبوع واحد
+ - %d أسبوعان
+ - %d أسابيع
+ - %d أسبوع
+ - %d أسبوع
+
+ يبدأ %1$s
+ ينتهي %1$s
+ الانتقال إلى اليوم
+ تحديث
+ اسم التطبيق
+ تعتيم الأحداث المكتملة
+ تعتيم الأحداث التي انتهت بالفعل في عرض الشهر والأسبوع
+ زر اليوم في شريط الأدوات
+ إظهار زر الانتقال إلى اليوم في شريط الأدوات بدلاً من زر عائم
+ اعرض Calendula كـ \"Calendar\" في مشغّل التطبيقات الخاص بك. فقط الاسم في المشغّل يتغير؛ وقد ينتقل الرمز إلى مكان جديد بعد التبديل.
+ تعتيم
+ الأحداث السابقة
+ مدى الجدول
+ مخصص…
+ أيام
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 57320a3..e03665b 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -354,7 +354,7 @@
Pomóż w tłumaczeniu
Dodaj lub ulepsz tłumaczenie w Weblate
Motyw, domyślny widok, pierwszy dzień tygodnia
- Kolejność przycisku szybkiego przełączania oraz menu
+ Układ widoku miesiąca, przycisk szybkiego przełączania, kolejność menu
Domyślne pola dla nowych wydarzeń
Przypomnienia o wydarzeniach
Urodziny i rocznice kontaktów
@@ -517,4 +517,32 @@
Zachowaj dzisiejszy dzień na górze agendy i widżetu, nawet gdy nie ma już na dziś żadnych zadań.
Urodziny
Rocznice
+ Strefa czasowa urządzenia
+ Podąża za Twoją lokalizacją
+ Znajdź strefę czasową
+ Ostatnie
+ Wszystkie strefy czasowe
+ Żadna strefa czasowa nie pasuje do „%1$s”
+ %1$s Twojego czasu
+ Wpisz liczbę od 1 do 999
+ Początek o %1$s
+ Koniec o %1$s
+ Dzisiaj
+ Przycisk „Dzisiaj” na pasku narzędzi
+ Pokaż przycisk skoku do dzisiaj na pasku narzędzi zamiast przycisku pływającego
+ Nazwa aplikacji
+ Wyświetlaj Calendula jako „Kalendarz” w menu aplikacji. Zmieni się tylko nazwa w menu; po przełączeniu ikona może pojawić się w innym miejscu.
+ Widok miesiąca
+ Styl widoku miesiąca
+ Strony
+ Jeden miesiąc wypełnia ekran. Przesuń w lewo lub w prawo, aby zmienić miesiąc.
+ Przewijanie miesięcy
+ Każdy miesiąc znajduje się pod własnym nagłówkiem, oddzielony od następnego niewielkim odstępem.
+ Tygodnie bez przerw
+ Tygodnie są ułożone jeden po drugim, każdy miesiąc płynnie łączy się z następnym, bez odstępu pomiędzy.
+ Podzielony
+ Zwięzła siatka wyróżnia dni z wydarzeniami a lista dla dnia wybranego dotknięciem wyświetla się poniżej.
+ Brak planów
+ Pokaż cały miesiąc
+ Pokaż wydarzenia dnia
From c70412c782d502a84147249f50a13c90778d460e Mon Sep 17 00:00:00 2001
From: Jean-Luc Makiola
Date: Tue, 28 Jul 2026 17:10:52 +0000
Subject: [PATCH 4/5] Link the privacy policy from the about card (!103)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a fourth link to the about card in settings, next to source and licence.
Play requires the privacy policy to be reachable from inside the app, not just from the store listing, for anything that touches calendar or contact data. Points at jeanlucmakiola.de/calendula/privacy.
Only the base string is added — Weblate owns the translations.
Reviewed-on: https://gitea.jeanlucmakiola.de/makiolaj/calendula/pulls/103
---
.../calendula/ui/settings/SettingsScreen.kt | 10 +++++++++-
app/src/main/res/values/strings.xml | 2 ++
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt
index c490375..c1efe32 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt
@@ -56,6 +56,7 @@ import androidx.compose.material.icons.filled.Keyboard
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette
+import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material.icons.filled.UploadFile
@@ -408,7 +409,9 @@ private fun LanguageRow(position: Position) {
@Composable
private fun AboutCard() {
// The card layout lives in floret-kit (components.AboutCard); Calendula
- // supplies its own logo, author and the source / licence / support links.
+ // supplies its own logo, author and the source / licence / privacy / support
+ // links. The privacy policy has to be reachable from inside the app, not just
+ // from the store listing, because Calendula touches calendar and contact data.
AboutCard(
logo = { AppLogo() },
appName = stringResource(R.string.app_name),
@@ -424,6 +427,11 @@ private fun AboutCard() {
label = stringResource(R.string.settings_license),
url = stringResource(R.string.about_license_url),
),
+ AboutLink(
+ icon = Icons.Default.PrivacyTip,
+ label = stringResource(R.string.settings_about_privacy),
+ url = stringResource(R.string.about_privacy_url),
+ ),
),
highlightLink = AboutLink(
icon = Icons.Default.Favorite,
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef9bfce..99407b9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -462,6 +462,7 @@
MIT
by Jean-Luc Makiola
Source
+ Privacy policy
Support development
Version %1$s
Calendula app icon
@@ -572,6 +573,7 @@
https://gitea.jeanlucmakiola.de/makiolaj/calendula
https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/LICENSE
+ https://jeanlucmakiola.de/calendula/privacy
https://ko-fi.com/jeanlucmakiola
https://weblate.dev.jeanlucmakiola.de/engage/calendula/
From db7094c54e328bb03d549c92c4f44eb4374e4155 Mon Sep 17 00:00:00 2001
From: Jean-Luc Makiola
Date: Wed, 29 Jul 2026 20:33:56 +0200
Subject: [PATCH 5/5] 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