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
5.4 KiB
Contributing to Agendula
Thanks for your interest in Agendula — a Material 3 Expressive task app that's a
pure front-end over the OpenTasks TaskContract provider, with no own database
or sync stack. Before diving in, skim docs/ARCHITECTURE.md
(how it's built), docs/ROADMAP.md (what's next), and
docs/PLAN.md (the design rationale). This file covers the
practical how.
The one architectural rule
Everything above the data layer talks to TasksRepository and sees only domain
types and Flows. Provider column names, TaskContract, ContentResolver, and
the authority string never leak above data/tasks/. This is what keeps
"Posture B" (bundling the provider later) an additive change instead of a
rewrite — see docs/ARCHITECTURE.md §7. If a change
would expose provider details to a ViewModel or the UI, it's in the wrong layer.
Prerequisites
- JDK 17
- Android SDK: compileSdk 37, build-tools 36.0.0 (the Gradle wrapper handles AGP/Kotlin)
- A device or emulator with OpenTasks or tasks.org installed for
anything touching the read/write paths (ideally with DAVx5 syncing a CalDAV
task list, so there's real data). Debug builds fall back to
DemoSeederfor sample data.
Build, test, lint
./gradlew :app:assembleDebug # build the debug APK
./gradlew :app:testDebugUnitTest # JVM unit tests (JUnit5 + Truth + Turbine)
./gradlew lintDebug # Android lint (CI runs this on every PR)
CI (.forgejo/workflows/ci.yaml, on Codeberg) runs a reproducible-release invariant check,
then lint → unit tests → debug build on every pull request, so run these locally
before opening a PR. Keep CI green.
Translations
Never edit a values-*/strings.xml file in a pull request. Translations are
owned by a self-hosted Weblate that writes to this repository directly, and a
hand-edit is overwritten on the next sync.
→ Translate Agendula on Weblate
Adding a new English string to values/strings.xml is normal PR work; Weblate
picks it up and offers it to translators. Partial translations are expected and
fine — missing keys are informational. Stale and orphaned keys are not, so run
python3 scripts/check_translations.py
before pushing. It reports those more clearly than lint's MissingTranslation
does, and it's what the Translations check runs on every PR.
A new language also needs one <locale> line in
app/src/main/res/xml/locales_config.xml — that file is the single source of
truth for both the in-app picker and the Android 13+ per-app language setting.
Where to put code
| Layer | Lives in | Rule of thumb |
|---|---|---|
| Pure logic (models, filtering, sorting, form validation, date maths) | domain/ |
No Android imports — must be JVM-unit-testable. |
| Provider access | data/tasks/ |
The only place that knows about the provider. New provider work goes through TasksDataSource. |
| Reminders, prefs, DI, demo data | data/reminders/, data/prefs/, data/di/, data/demo/ |
|
| Screens | ui/<area>/ |
One ViewModel + immutable UiState per area; Compose for the screen. |
Code style & conventions
- Kotlin, 4-space indent, LF line endings, final newline, no trailing
whitespace — all enforced by
.editorconfig(2-space for yaml/toml/json/md). Match the surrounding code. - Material 3 Expressive for all UI: use
MaterialExpressiveTheme, the colour-scheme tokens (never hardcoded colours), and canonical M3 components (e.g.ListItemfor rows). Consult thematerial-3skill before designing a new screen or component. - Prefer the domain layer for anything testable; keep
AndroidTasksDataSourcethe only Android-coupled data implementation so the rest stays JVM-testable.
Tests
- New domain logic (mappers, filters, sorting, forms, value mapping) must
come with JVM unit tests under
app/src/test/. The data source is the JVM-testable seam — mock or fake it rather than reaching for instrumentation. - Add an instrumented test only when a path genuinely needs a real
ContentResolver.
Commits & PRs
- Write focused commits with clear messages (the existing history uses short,
scoped subjects like
UI: .../M1 ...). - Update
CHANGELOG.mdunder[Unreleased]for any user-visible change — its sections feed the release notes and F-Droid "What's New" (seedocs/RELEASING.md). - If your change shifts the architecture or completes a milestone, update
docs/ARCHITECTURE.md/docs/ROADMAP.mdin the same PR. - Don't bump
versionName/versionCodein a regular PR — the committedversionNameis bumped only when cutting a release (that bump reachingmainis what triggers the release; the pipeline then mints the tag). Seedocs/RELEASING.md.
Scope
Agendula stays true to its thesis: a front-end over open task backends (CalDAV / iCalendar / DecSync via the OpenTasks provider). Proprietary backends (Google Tasks, Microsoft To Do) are out of scope by design — they'd mean owning a sync stack. v1 targets the OpenTasks contract (OpenTasks + tasks.org); jtx's richer contract is a possible later addition.
License
By contributing you agree your contributions are licensed under the project's MIT License.