Compare commits
7 Commits
v2.16.0
...
9697b56404
| Author | SHA1 | Date | |
|---|---|---|---|
| 9697b56404 | |||
| c70412c782 | |||
| ac0c43f930 | |||
| 314236ac0c | |||
| c6e83fc071 | |||
| 4c1bfc052e | |||
| bba536394f |
@@ -1,4 +1,4 @@
|
||||
name: Release — F-Droid repo + Gitea/Codeberg release
|
||||
name: Release — F-Droid repo + Gitea/Codeberg release + Play
|
||||
|
||||
# A release is cut by merging a release branch into main with a bumped
|
||||
# versionName (see docs/RELEASING.md). This workflow reads that versionName and,
|
||||
@@ -9,6 +9,12 @@ name: Release — F-Droid repo + Gitea/Codeberg release
|
||||
# trigger. Ordinary merges (no version bump) fall through `detect` and do
|
||||
# nothing.
|
||||
#
|
||||
# A trailing `play` job then uploads the App Bundle to Google Play. It is last
|
||||
# and separate because Play is the only channel that can reject a good build for
|
||||
# reasons the pipeline can't see, and that must not endanger a release which has
|
||||
# already shipped to F-Droid and Codeberg. It skips cleanly until the
|
||||
# PLAY_SERVICE_ACCOUNT_JSON secret exists.
|
||||
#
|
||||
# A manual workflow_dispatch (from a branch) runs the re-sign-only recovery
|
||||
# path: it re-signs the existing F-Droid index with the repo key and re-uploads,
|
||||
# without building an APK or creating a release. Used for key rotation / repo
|
||||
@@ -174,6 +180,33 @@ jobs:
|
||||
if: env.IS_RELEASE == 'true'
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
# Play takes an App Bundle, not the APK, so it is a second artifact from
|
||||
# the same source and the same signing config — not a repackage of the
|
||||
# APK. The release key signs it, but Play only ever treats that key as the
|
||||
# *upload* key: Play App Signing re-signs with Google's own key before
|
||||
# delivery. A Play install and an F-Droid install therefore carry
|
||||
# different signatures and cannot update each other. That divergence is a
|
||||
# deliberate, documented choice (docs/RELEASING.md), not an accident.
|
||||
#
|
||||
# Nothing here touches the F-Droid path: the AAB is never copied into the
|
||||
# repo, never attached to a release, and its build cannot change the APK
|
||||
# that was already produced above.
|
||||
#
|
||||
# AGP embeds the R8 mapping in the bundle's BUNDLE-METADATA, so Play gets
|
||||
# deobfuscated stacktraces without a separate mapping upload.
|
||||
- name: Build release AAB
|
||||
if: env.IS_RELEASE == 'true'
|
||||
run: ./gradlew bundleRelease
|
||||
|
||||
- name: Hand the AAB to the Play job
|
||||
if: env.IS_RELEASE == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-aab-${{ needs.detect.outputs.version }}
|
||||
path: app/build/outputs/bundle/release/app-release.aab
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Setup F-Droid Server Tools
|
||||
run: |
|
||||
SUDO=""
|
||||
@@ -412,20 +445,31 @@ jobs:
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
# Upsert (re-run safe). POST also creates the tag at target_commitish
|
||||
# if the push mirror hasn't synced it yet.
|
||||
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
||||
if [ -n "$ID" ]; then
|
||||
# Create (or update) the release. Codeberg 500s on a POST/GET against a
|
||||
# tag it has only just received — the release request outruns the
|
||||
# indexing of the ref we pushed a moment ago — so a single attempt kept
|
||||
# failing and skipping the mirror even though the very same call
|
||||
# succeeds seconds later. Retry with backoff, and PATCH in place if a
|
||||
# release already exists (re-run safe). A 5xx body still exits curl 0,
|
||||
# so the loop, not `set -e`, controls the flow.
|
||||
ID=""
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
EXIST=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty' 2>/dev/null || true)
|
||||
if [ -n "$EXIST" ]; then
|
||||
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d @cb-payload.json "$API/releases/$ID"
|
||||
else
|
||||
curl -s -o cb-response.json -w "release POST HTTP %{http_code}\n" -X POST \
|
||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d @cb-payload.json "$API/releases"
|
||||
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
||||
-d @cb-payload.json "$API/releases/$EXIST"
|
||||
ID="$EXIST"; break
|
||||
fi
|
||||
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id." >&2; exit 1; fi
|
||||
CODE=$(curl -s -o cb-response.json -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d @cb-payload.json "$API/releases")
|
||||
echo "release POST attempt $attempt HTTP $CODE"
|
||||
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
||||
[ -n "$ID" ] && break
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id after retries." >&2; exit 1; fi
|
||||
|
||||
# Attach APK + checksum, replacing any prior asset of the same name.
|
||||
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
||||
@@ -437,3 +481,123 @@ jobs:
|
||||
"$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n"
|
||||
done
|
||||
echo "Published $TAG to Codeberg."
|
||||
|
||||
# Google Play channel.
|
||||
#
|
||||
# A separate job, on purpose, running only AFTER the F-Droid publish and both
|
||||
# forge releases have completed. Play is the one channel that can reject a
|
||||
# perfectly good build for reasons outside the pipeline (listing rules, policy
|
||||
# review, API outage, a track that needs manual promotion). Isolating it means
|
||||
# such a rejection surfaces as one red job next to a release that already
|
||||
# shipped everywhere else, instead of failing the workflow that publishes it.
|
||||
#
|
||||
# Not a `container:` job even though a fastlane image exists: act_runner does
|
||||
# not provide node inside custom job containers, so JavaScript actions
|
||||
# (checkout, download-artifact) can't run there. The Renovate job gets away
|
||||
# with a container because its only step is a shell command. Ruby is installed
|
||||
# the same way sshpass, jq and fdroidserver are in the job above.
|
||||
play:
|
||||
needs: [detect, release]
|
||||
# workflow_dispatch is the F-Droid re-sign recovery path — it must never
|
||||
# touch Play, so gate on a real release only.
|
||||
if: needs.detect.outputs.is_release == 'true'
|
||||
runs-on: docker
|
||||
env:
|
||||
VERSION: ${{ needs.detect.outputs.version }}
|
||||
VERSION_CODE: ${{ needs.detect.outputs.version_code }}
|
||||
# Where the bundle lands. `internal` by default so a release reaches
|
||||
# testers rather than the public, and promotion to production stays a
|
||||
# deliberate human action in the Play Console — the same posture as
|
||||
# holding UI releases for on-device review. Override with the PLAY_TRACK
|
||||
# repo variable once the flow is trusted.
|
||||
PLAY_TRACK: ${{ vars.PLAY_TRACK || 'internal' }}
|
||||
PLAY_RELEASE_STATUS: ${{ vars.PLAY_RELEASE_STATUS || 'completed' }}
|
||||
# Set PLAY_DRY_RUN=true to validate the edit against the API and discard
|
||||
# it instead of committing — used to rehearse the first upload.
|
||||
PLAY_DRY_RUN: ${{ vars.PLAY_DRY_RUN || 'false' }}
|
||||
BUNDLE_PATH: vendor/bundle
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Skip cleanly (not fatally) when Play isn't configured yet, so the rest
|
||||
# of the release pipeline keeps working during setup — same contract as
|
||||
# the Codeberg mirror step.
|
||||
- name: Write the Play service-account key
|
||||
id: key
|
||||
env:
|
||||
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${PLAY_SERVICE_ACCOUNT_JSON:-}" ]; then
|
||||
echo "PLAY_SERVICE_ACCOUNT_JSON not set — skipping the Play upload."
|
||||
echo "configured=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s' "$PLAY_SERVICE_ACCOUNT_JSON" > play-service-account.json
|
||||
# Fail here, with a clear message, rather than inside fastlane: a
|
||||
# mangled multi-line secret is the likeliest setup mistake.
|
||||
python3 -c "import json,sys; d=json.load(open('play-service-account.json')); sys.exit(0 if d.get('type')=='service_account' else 1)" \
|
||||
|| { echo "PLAY_SERVICE_ACCOUNT_JSON is not a valid service-account JSON." >&2; exit 1; }
|
||||
echo "configured=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download the AAB
|
||||
if: steps.key.outputs.configured == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-aab-${{ needs.detect.outputs.version }}
|
||||
path: dist
|
||||
|
||||
- name: Install Ruby
|
||||
if: steps.key.outputs.configured == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SUDO=""
|
||||
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
|
||||
$SUDO apt-get update
|
||||
# ruby-dev + build-essential: several of fastlane's dependencies build
|
||||
# native extensions.
|
||||
$SUDO apt-get install -y ruby-full ruby-dev build-essential
|
||||
ruby -v
|
||||
|
||||
# Only the first release pays the full gem build; afterwards this restores.
|
||||
- name: Cache bundled gems
|
||||
if: steps.key.outputs.configured == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gems-
|
||||
|
||||
- name: Install fastlane
|
||||
if: steps.key.outputs.configured == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gem install bundler --no-document
|
||||
bundle config set --local path vendor/bundle
|
||||
bundle install --jobs 4
|
||||
bundle exec fastlane --version
|
||||
|
||||
- name: Upload to Play
|
||||
if: steps.key.outputs.configured == 'true'
|
||||
env:
|
||||
SUPPLY_JSON_KEY: play-service-account.json
|
||||
# supply is chatty on a TTY-less runner otherwise.
|
||||
FASTLANE_SKIP_UPDATE_CHECK: '1'
|
||||
FASTLANE_HIDE_CHANGELOG: '1'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f "dist/app-release.aab"
|
||||
bundle exec fastlane deploy \
|
||||
aab:"dist/app-release.aab" \
|
||||
track:"$PLAY_TRACK" \
|
||||
release_status:"$PLAY_RELEASE_STATUS" \
|
||||
dry_run:"$PLAY_DRY_RUN"
|
||||
echo "Uploaded $VERSION (code $VERSION_CODE) to the '$PLAY_TRACK' track."
|
||||
|
||||
# The workspace is reused between runs on a self-hosted runner, so the
|
||||
# credential must not outlive the job.
|
||||
- name: Shred the service-account key
|
||||
if: always()
|
||||
run: shred -u play-service-account.json 2>/dev/null || rm -f play-service-account.json
|
||||
|
||||
@@ -39,4 +39,9 @@ jobs:
|
||||
RENOVATE_REPOSITORIES: '["makiolaj/calendula"]'
|
||||
# Commits/PRs authored as the bot, not a real maintainer.
|
||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@jeanlucmakiola.de>'
|
||||
# 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
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -56,5 +56,16 @@ Thumbs.db
|
||||
# KSP
|
||||
.ksp/
|
||||
|
||||
# Google Play Developer API service-account key. Reconstructed in CI from the
|
||||
# PLAY_SERVICE_ACCOUNT_JSON secret and shredded afterwards — never committed.
|
||||
/play-service-account.json
|
||||
|
||||
# fastlane (Play uploader only — see fastlane/Fastfile)
|
||||
/fastlane/report.xml
|
||||
/fastlane/README.md
|
||||
/vendor/bundle/
|
||||
/.bundle/
|
||||
Gemfile.lock
|
||||
|
||||
# Claude Code
|
||||
/CLAUDE.md
|
||||
|
||||
10
Gemfile
Normal file
10
Gemfile
Normal file
@@ -0,0 +1,10 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
# fastlane is used ONLY to upload the release bundle to Google Play
|
||||
# (see fastlane/Fastfile). It is not part of the build or the signing path, so
|
||||
# it never runs on a PR — only in release.yaml's `play` job.
|
||||
#
|
||||
# Pinned exactly; Renovate's bundler manager keeps it bumped. No Gemfile.lock is
|
||||
# committed on purpose: this resolves an uploader's transitive deps, not the
|
||||
# app's, and none of it affects the reproducible release build.
|
||||
gem "fastlane", "2.237.0"
|
||||
@@ -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,
|
||||
|
||||
@@ -53,11 +53,11 @@
|
||||
<string name="event_share_failed">تعذّر مشاركة هذا الحدث.</string>
|
||||
<string name="event_delete_title">حذف الحدث؟</string>
|
||||
<string name="event_delete_body">الحدث أُزيل من تقويمك ومن كل جهاز تتم مزامنته معه.</string>
|
||||
<string name="event_delete_recurring_title">حذف الحدث المتكرر</string>
|
||||
<string name="event_delete_recurring_title">حذف الحدث المتكرّر</string>
|
||||
<string name="event_delete_option_occurrence">فقط هذا الحدث</string>
|
||||
<string name="event_delete_option_following">هذا وجميع الأحداث اللاحقة</string>
|
||||
<string name="event_delete_option_series">جميع الأحداث في السلسلة</string>
|
||||
<string name="event_edit_recurring_title">تعديل الحدث المتكرر</string>
|
||||
<string name="event_edit_recurring_title">تعديل الحدث المتكرّر</string>
|
||||
<string name="event_delete_failed">تعذّر حذف الحدث</string>
|
||||
<string name="event_delete_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
||||
<string name="dialog_cancel">إلغاء</string>
|
||||
@@ -108,7 +108,7 @@
|
||||
<string name="event_edit_conflict_discard_hint">الحدث يبقى كما هو الآن</string>
|
||||
<string name="import_reminder_prompt_title">هل تريد تطبيق تذكيرك الافتراضي؟</string>
|
||||
<string name="event_edit_gone_body">هذا الحدث حُذف في هذه الأثناء، على سبيل المثال على جهاز آخر. لم يعد من الممكن حفظ تغييراتك.</string>
|
||||
<string name="event_edit_more_fields">المزيد من الخيارات</string>
|
||||
<string name="event_edit_more_fields">المزيد من الخانات</string>
|
||||
<string name="event_access_public">علني</string>
|
||||
<string name="event_access_default">الافتراضي</string>
|
||||
<string name="event_availability_busy">مشغول</string>
|
||||
@@ -153,7 +153,7 @@
|
||||
<string name="recurrence_with_until">%1$s حتى %2$s</string>
|
||||
<string name="recurrence_with_count">%1$s، %2$d مرّات</string>
|
||||
<string name="import_reminder_prompt_apply">طَبِّق الافتراضي</string>
|
||||
<string name="event_detail_recurring">حدث مُتكرر</string>
|
||||
<string name="event_detail_recurring">حدث متكرّر</string>
|
||||
<string name="recurrence_on_days">%1$s في %2$s</string>
|
||||
<string name="event_attendee_unknown">—</string>
|
||||
<string name="event_attendee_accepted">تم القُبول</string>
|
||||
@@ -166,7 +166,7 @@
|
||||
<string name="event_status_cancelled">ملغي</string>
|
||||
<string name="event_availability_free">متاح</string>
|
||||
<string name="event_access_private">خاص</string>
|
||||
<string name="event_access_confidential">سري</string>
|
||||
<string name="event_access_confidential">سرّي</string>
|
||||
<string name="event_attendee_organizer">المُنظِّم</string>
|
||||
<string name="event_attendee_optional">اختياري</string>
|
||||
<string name="reminder_default">التذكير الافتراضي</string>
|
||||
@@ -180,7 +180,7 @@
|
||||
<string name="reminder_onboarding_body">Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
|
||||
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string>
|
||||
<string name="reminder_benefit_duplicates_title">هل تستخدم تطبيق تقويم ثانٍ؟</string>
|
||||
<string name="reminder_benefit_duplicates_body">إذا كان تطبيق آخر أيضًا ينشر تذكيرات، فستراهم مرتين — قم بإيقاف تشغيلهم هناك أو هنا.</string>
|
||||
<string name="reminder_benefit_duplicates_body">إذا تطبيق آخر أيضًا ينشر تذكيرات، فستراهم مرتين — قم بإيقاف تشغيلهم هناك أو هنا.</string>
|
||||
<string name="reminder_benefit_reversible_title">يمكنك تغييره في أي وقت</string>
|
||||
<string name="reminder_onboarding_enable_button">شَغِّل التذكيرات</string>
|
||||
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
||||
@@ -200,7 +200,7 @@
|
||||
<string name="search_hint">البحث عن الأحداث</string>
|
||||
<string name="search_clear">مسح</string>
|
||||
<string name="agenda_no_more_today">لا مزيد من الأحداث اليوم</string>
|
||||
<string name="search_empty">لا توجد أحداث مطابقة ”%1$s“.</string>
|
||||
<string name="search_empty">لا أحداث مطابقة ”%1$s“.</string>
|
||||
<string name="search_idle_hint">ابحث عن أحداثك بالعنوان، الموقع أو الملاحظات.</string>
|
||||
<string name="search_back">الرجوع</string>
|
||||
<string name="search_action">البحث</string>
|
||||
@@ -217,7 +217,7 @@
|
||||
<string name="back">رجوع</string>
|
||||
<string name="settings_section_appearance">المظهر</string>
|
||||
<string name="settings_dynamic_color">اللون الديناميكي</string>
|
||||
<string name="settings_dynamic_color_unavailable">يتطلب Android 12 أو أحدث</string>
|
||||
<string name="settings_dynamic_color_unavailable">يتطلب أندرويد ١٢ أو أحدث</string>
|
||||
<string name="settings_default_view">طريقة العرض الافتراضية</string>
|
||||
<string name="settings_soften_colors">ألوان تقويم ناعمة</string>
|
||||
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
||||
@@ -227,7 +227,7 @@
|
||||
<string name="settings_font_choose_file">اختر ملفًا…</string>
|
||||
<string name="settings_font_custom_selected">خط مخصص</string>
|
||||
<string name="settings_font_import_failed">تعذّر قراءة هذا الملف كخط</string>
|
||||
<string name="settings_week_start">يبدأ الأسبوع في</string>
|
||||
<string name="settings_week_start">الأسبوع يبدأ في</string>
|
||||
<string name="settings_week_start_auto">تلقائي</string>
|
||||
<string name="settings_time_format_auto">تلقائي</string>
|
||||
<string name="settings_time_format">تنسيق الوقت</string>
|
||||
@@ -243,4 +243,58 @@
|
||||
<string name="settings_past_events_show">إظهار</string>
|
||||
<string name="settings_past_events_hide">إخفاء</string>
|
||||
<string name="settings_agenda_header">جدول</string>
|
||||
<string name="event_edit_timezone_device">المنطقة الزمنية للجهاز</string>
|
||||
<string name="event_edit_timezone_search">البحث عن المناطق الزمنية</string>
|
||||
<string name="event_edit_timezone_all">جميع المناطق الزمنية</string>
|
||||
<string name="event_edit_timezone_none">لا منطقة زمنية تطابق \"%1$s\"</string>
|
||||
<string name="event_edit_timezone_local_time">%1$s توقيتك</string>
|
||||
<string name="event_edit_timezone_recent">الأحدث</string>
|
||||
<string name="event_edit_recurrence_incomplete">أدخل رقمًا من ١ إلى ٩٩٩</string>
|
||||
<plurals name="duration_minutes">
|
||||
<item quantity="zero">(%d) لا دقائق</item>
|
||||
<item quantity="one">(%d) دقيقة واحده</item>
|
||||
<item quantity="two">%d دقيقتان</item>
|
||||
<item quantity="few">%d دقائق</item>
|
||||
<item quantity="many">%d دقيقة</item>
|
||||
<item quantity="other">%d دقيقة</item>
|
||||
</plurals>
|
||||
<plurals name="duration_hours">
|
||||
<item quantity="zero">(%d) لا ساعات</item>
|
||||
<item quantity="one">(%d) ساعة واحده</item>
|
||||
<item quantity="two">%d ساعتان</item>
|
||||
<item quantity="few">%d ساعات</item>
|
||||
<item quantity="many">%d ساعة</item>
|
||||
<item quantity="other">%d ساعة</item>
|
||||
</plurals>
|
||||
<plurals name="duration_days">
|
||||
<item quantity="zero">(%d) لا أيام</item>
|
||||
<item quantity="one">(%d) يوم واحد</item>
|
||||
<item quantity="two">%d يومان</item>
|
||||
<item quantity="few">%d أيام</item>
|
||||
<item quantity="many">%d يوم</item>
|
||||
<item quantity="other">%d يوم</item>
|
||||
</plurals>
|
||||
<plurals name="duration_weeks">
|
||||
<item quantity="zero">(%d) لا أسابيع</item>
|
||||
<item quantity="one">(%d) أسبوع واحد</item>
|
||||
<item quantity="two">%d أسبوعان</item>
|
||||
<item quantity="few">%d أسابيع</item>
|
||||
<item quantity="many">%d أسبوع</item>
|
||||
<item quantity="other">%d أسبوع</item>
|
||||
</plurals>
|
||||
<string name="agenda_span_starts">يبدأ %1$s</string>
|
||||
<string name="agenda_span_ends">ينتهي %1$s</string>
|
||||
<string name="today_jump_action">الانتقال إلى اليوم</string>
|
||||
<string name="widget_refresh">تحديث</string>
|
||||
<string name="settings_app_name">اسم التطبيق</string>
|
||||
<string name="settings_dim_completed">تعتيم الأحداث المكتملة</string>
|
||||
<string name="settings_dim_completed_summary">تعتيم الأحداث التي انتهت بالفعل في عرض الشهر والأسبوع</string>
|
||||
<string name="settings_today_toolbar">زر اليوم في شريط الأدوات</string>
|
||||
<string name="settings_today_toolbar_summary">إظهار زر الانتقال إلى اليوم في شريط الأدوات بدلاً من زر عائم</string>
|
||||
<string name="settings_app_name_summary">اعرض Calendula كـ \"Calendar\" في مشغّل التطبيقات الخاص بك. فقط الاسم في المشغّل يتغير؛ وقد ينتقل الرمز إلى مكان جديد بعد التبديل.</string>
|
||||
<string name="settings_past_events_dim">تعتيم</string>
|
||||
<string name="settings_past_events">الأحداث السابقة</string>
|
||||
<string name="settings_agenda_range">مدى الجدول</string>
|
||||
<string name="agenda_range_custom">مخصص…</string>
|
||||
<string name="agenda_range_custom_hint">أيام</string>
|
||||
</resources>
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
<string name="settings_translate">Pomóż w tłumaczeniu</string>
|
||||
<string name="settings_translate_hint">Dodaj lub ulepsz tłumaczenie w Weblate</string>
|
||||
<string name="settings_appearance_subtitle">Motyw, domyślny widok, pierwszy dzień tygodnia</string>
|
||||
<string name="settings_views_subtitle">Kolejność przycisku szybkiego przełączania oraz menu</string>
|
||||
<string name="settings_views_subtitle">Układ widoku miesiąca, przycisk szybkiego przełączania, kolejność menu</string>
|
||||
<string name="settings_event_form_subtitle">Domyślne pola dla nowych wydarzeń</string>
|
||||
<string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
|
||||
<string name="settings_special_dates_subtitle">Urodziny i rocznice kontaktów</string>
|
||||
@@ -517,4 +517,32 @@
|
||||
<string name="settings_agenda_show_today_hint">Zachowaj dzisiejszy dzień na górze agendy i widżetu, nawet gdy nie ma już na dziś żadnych zadań.</string>
|
||||
<string name="special_dates_calendar_birthday">Urodziny</string>
|
||||
<string name="special_dates_calendar_anniversary">Rocznice</string>
|
||||
<string name="event_edit_timezone_device">Strefa czasowa urządzenia</string>
|
||||
<string name="event_edit_timezone_device_summary">Podąża za Twoją lokalizacją</string>
|
||||
<string name="event_edit_timezone_search">Znajdź strefę czasową</string>
|
||||
<string name="event_edit_timezone_recent">Ostatnie</string>
|
||||
<string name="event_edit_timezone_all">Wszystkie strefy czasowe</string>
|
||||
<string name="event_edit_timezone_none">Żadna strefa czasowa nie pasuje do „%1$s”</string>
|
||||
<string name="event_edit_timezone_local_time">%1$s Twojego czasu</string>
|
||||
<string name="event_edit_recurrence_incomplete">Wpisz liczbę od 1 do 999</string>
|
||||
<string name="agenda_span_starts">Początek o %1$s</string>
|
||||
<string name="agenda_span_ends">Koniec o %1$s</string>
|
||||
<string name="today_jump_action">Dzisiaj</string>
|
||||
<string name="settings_today_toolbar">Przycisk „Dzisiaj” na pasku narzędzi</string>
|
||||
<string name="settings_today_toolbar_summary">Pokaż przycisk skoku do dzisiaj na pasku narzędzi zamiast przycisku pływającego</string>
|
||||
<string name="settings_app_name">Nazwa aplikacji</string>
|
||||
<string name="settings_app_name_summary">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.</string>
|
||||
<string name="settings_month_header">Widok miesiąca</string>
|
||||
<string name="settings_month_view_style">Styl widoku miesiąca</string>
|
||||
<string name="month_style_paged">Strony</string>
|
||||
<string name="month_style_paged_summary">Jeden miesiąc wypełnia ekran. Przesuń w lewo lub w prawo, aby zmienić miesiąc.</string>
|
||||
<string name="month_style_continuous">Przewijanie miesięcy</string>
|
||||
<string name="month_style_continuous_summary">Każdy miesiąc znajduje się pod własnym nagłówkiem, oddzielony od następnego niewielkim odstępem.</string>
|
||||
<string name="month_style_dense">Tygodnie bez przerw</string>
|
||||
<string name="month_style_dense_summary">Tygodnie są ułożone jeden po drugim, każdy miesiąc płynnie łączy się z następnym, bez odstępu pomiędzy.</string>
|
||||
<string name="month_style_split">Podzielony</string>
|
||||
<string name="month_style_split_summary">Zwięzła siatka wyróżnia dni z wydarzeniami a lista dla dnia wybranego dotknięciem wyświetla się poniżej.</string>
|
||||
<string name="month_split_no_events">Brak planów</string>
|
||||
<string name="month_split_expand">Pokaż cały miesiąc</string>
|
||||
<string name="month_split_collapse">Pokaż wydarzenia dnia</string>
|
||||
</resources>
|
||||
|
||||
@@ -462,6 +462,7 @@
|
||||
<string name="settings_license_value">MIT</string>
|
||||
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
||||
<string name="settings_about_source">Source</string>
|
||||
<string name="settings_about_privacy">Privacy policy</string>
|
||||
<string name="settings_about_support">Support development</string>
|
||||
<string name="settings_about_version">Version %1$s</string>
|
||||
<string name="settings_about_logo_desc">Calendula app icon</string>
|
||||
@@ -572,6 +573,7 @@
|
||||
|
||||
<string name="about_source_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula</string>
|
||||
<string name="about_license_url" translatable="false">https://gitea.jeanlucmakiola.de/makiolaj/calendula/src/branch/main/LICENSE</string>
|
||||
<string name="about_privacy_url" translatable="false">https://jeanlucmakiola.de/calendula/privacy</string>
|
||||
<string name="about_support_url" translatable="false">https://ko-fi.com/jeanlucmakiola</string>
|
||||
<string name="about_translate_url" translatable="false">https://weblate.dev.jeanlucmakiola.de/engage/calendula/</string>
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ release work when a merge actually cuts a release:
|
||||
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.
|
||||
- **`play` job** (same workflow, after `release`) — uploads the App Bundle to
|
||||
Google Play. Runs last and separately so a Play rejection can't endanger a
|
||||
release that already shipped; skips cleanly until Play is configured.
|
||||
|
||||
### Codeberg direct-download channel
|
||||
|
||||
@@ -101,6 +104,58 @@ unset. One-time setup: the repo's **Releases** unit must be enabled and a
|
||||
`CODEBERG_RELEASE_TOKEN` secret (Codeberg access token, `write:repository` scope) added
|
||||
to Gitea Actions.
|
||||
|
||||
### Google Play channel
|
||||
|
||||
Play is a third channel alongside F-Droid and the Codeberg download, and it is
|
||||
the only one that gets a different artifact and a different signature.
|
||||
|
||||
**Artifact.** Play takes an **App Bundle** (`bundleRelease`), not the APK. It is
|
||||
a second output of the same source and the same signing config — never a
|
||||
repackage of the published APK, which stays untouched so the F-Droid
|
||||
reproducibility guarantee is unaffected. `dependenciesInfo` stays disabled for
|
||||
the bundle too; Play's "app dependencies" report is optional and re-enabling it
|
||||
would break reproducibility.
|
||||
|
||||
**Signature — read this before assuming an update path exists.** Play App
|
||||
Signing is mandatory for new apps, and Google generates and holds the app
|
||||
signing key. The release keystore in CI is registered only as the **upload
|
||||
key**: Play verifies uploads with it, then re-signs with Google's key before
|
||||
delivery. Consequences, accepted deliberately:
|
||||
|
||||
- A Play install and an F-Droid install have **different signatures** and
|
||||
**cannot update each other**. Switching channels requires uninstall +
|
||||
reinstall, which loses nothing (all data lives in the system calendar
|
||||
provider) but must be stated wherever both channels are advertised.
|
||||
- Losing the upload key is **recoverable** — request an upload-key reset in the
|
||||
Play Console. Losing the app key still is not, for F-Droid.
|
||||
|
||||
**Build and signing are not fastlane's job.** Gradle does both, exactly as
|
||||
before. fastlane appears only as the Play Developer API client (`supply`),
|
||||
because the store listing already lives in `fastlane/metadata/android/` — the
|
||||
same tree the official F-Droid repo harvests. One metadata source, two stores.
|
||||
|
||||
**What gets uploaded per release:** the AAB, plus the per-version "What's New"
|
||||
from `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` (generated
|
||||
from `CHANGELOG.md` by `scripts/sync_changelog_to_fastlane.sh`). Listing text is
|
||||
**not** touched — an accidental overwrite of a live listing triggers a Play
|
||||
policy review. Sync it deliberately with `bundle exec fastlane listing`.
|
||||
|
||||
**Screenshots and graphics are skipped**, because the committed assets satisfy
|
||||
F-Droid but not Play:
|
||||
|
||||
| Asset | Committed | Play requires |
|
||||
| --- | --- | --- |
|
||||
| `phoneScreenshots/*.png` | 1280×2856, 32-bit RGBA | long edge ≤ 2× short edge (so ≤ 2560), 24-bit PNG, no alpha |
|
||||
| `icon.png` | 512×512, 24-bit RGB | 512×512, 32-bit PNG |
|
||||
| `featureGraphic.png` | *missing* | required, exactly 1024×500 |
|
||||
|
||||
Until those are fixed, Play's graphics are managed by hand in the console. Then
|
||||
pass `images:true` to the `listing` lane.
|
||||
|
||||
**Track.** Uploads go to `internal` by default; promotion to production stays a
|
||||
manual action in the Play Console, matching the rule that UI releases wait for
|
||||
on-device review. Override with the `PLAY_TRACK` repo variable.
|
||||
|
||||
### Manual re-sign / recovery
|
||||
|
||||
A manual `workflow_dispatch` of the release workflow runs a **re-sign-only**
|
||||
@@ -119,6 +174,15 @@ for key rotation or repo recovery without publishing a new app version.
|
||||
| `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. |
|
||||
| `PLAY_SERVICE_ACCOUNT_JSON` | Google Cloud service-account key (full JSON) with Play Console access — uploads the AAB. If unset, the `play` job skips cleanly. |
|
||||
|
||||
### Variables (Gitea → repo Settings → Actions → Variables)
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `PLAY_TRACK` | `internal` | Play track the bundle is uploaded to. |
|
||||
| `PLAY_RELEASE_STATUS` | `completed` | `completed`, `draft`, `inProgress` or `halted`. |
|
||||
| `PLAY_DRY_RUN` | `false` | `true` validates the Play edit against the API and discards it — use to rehearse. |
|
||||
|
||||
The two keys are independent: the **app key** signs APKs; the **repo key**
|
||||
signs the index (its fingerprint is what users pin). Neither key nor the
|
||||
|
||||
7
fastlane/Appfile
Normal file
7
fastlane/Appfile
Normal file
@@ -0,0 +1,7 @@
|
||||
# fastlane is used in this repo ONLY as a Google Play Developer API client
|
||||
# (see fastlane/Fastfile). It never builds and never signs.
|
||||
|
||||
# Reconstructed in CI from the PLAY_SERVICE_ACCOUNT_JSON secret; gitignored.
|
||||
json_key_file(ENV["SUPPLY_JSON_KEY"] || "play-service-account.json")
|
||||
|
||||
package_name("de.jeanlucmakiola.calendula")
|
||||
80
fastlane/Fastfile
Normal file
80
fastlane/Fastfile
Normal file
@@ -0,0 +1,80 @@
|
||||
# Google Play publishing only.
|
||||
#
|
||||
# Building and signing are deliberately NOT fastlane's job: release.yaml drives
|
||||
# Gradle directly so the release build stays F-Droid-reproducible (`vcsInfo`,
|
||||
# `dependenciesInfo` and the AGP metadata block are disabled in
|
||||
# app/build.gradle.kts and guarded by scripts/check_reproducible_release.sh).
|
||||
# Interposing fastlane there would add a layer that can inject Gradle flags into
|
||||
# the one build whose byte-for-byte output is verified by a third party.
|
||||
#
|
||||
# What fastlane IS here for is `supply` — the Play Developer API client — because
|
||||
# the store listing already lives in fastlane/metadata/android, which the
|
||||
# official F-Droid repo harvests from the tagged source tree. One metadata tree,
|
||||
# two stores.
|
||||
|
||||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Upload an already-built, already-signed AAB to Play"
|
||||
lane :deploy do |options|
|
||||
aab = options[:aab] || "app/build/outputs/bundle/release/app-release.aab"
|
||||
UI.user_error!("AAB not found at #{aab}") unless File.exist?(aab)
|
||||
|
||||
supply(
|
||||
aab: aab,
|
||||
track: options[:track] || "internal",
|
||||
release_status: options[:release_status] || "completed",
|
||||
|
||||
# The APK belongs to F-Droid and the Codeberg download; Play only ever
|
||||
# gets the bundle. Never let the uploader reach for the other artifact.
|
||||
skip_upload_apk: true,
|
||||
|
||||
# Listing text is synced deliberately via the `listing` lane, not on every
|
||||
# release. Overwriting a live listing by accident triggers a Play policy
|
||||
# review, which is not a quick revert.
|
||||
skip_upload_metadata: true,
|
||||
|
||||
# "What's New" DOES track every release.
|
||||
# scripts/sync_changelog_to_fastlane.sh writes
|
||||
# fastlane/metadata/android/en-US/changelogs/<versionCode>.txt from
|
||||
# CHANGELOG.md, so Play shows exactly the text F-Droid does.
|
||||
skip_upload_changelogs: false,
|
||||
|
||||
# See the `listing` lane for why the committed images can't go to Play yet.
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
|
||||
# Dry run: validate the edit against the API and roll it back instead of
|
||||
# committing it. Used for the first end-to-end rehearsal.
|
||||
validate_only: options[:dry_run].to_s == "true",
|
||||
)
|
||||
end
|
||||
|
||||
desc "Sync the store listing text (and, once the assets qualify, the images)"
|
||||
lane :listing do |options|
|
||||
# Deliberate and manual: `bundle exec fastlane listing`. Not wired into the
|
||||
# release pipeline.
|
||||
#
|
||||
# Images stay off by default because the committed assets do not satisfy
|
||||
# Play today, even though F-Droid accepts all of them:
|
||||
#
|
||||
# * phoneScreenshots are 1280x2856. Play caps the long edge at twice the
|
||||
# short edge (2560 here), so every screenshot is rejected.
|
||||
# * those PNGs are 32-bit RGBA. Play wants 24-bit PNG, no alpha.
|
||||
# * icon.png is 512x512 but 24-bit RGB. Play's icon wants 32-bit PNG.
|
||||
# * there is no featureGraphic.png. Play requires one, exactly 1024x500.
|
||||
#
|
||||
# Until that is fixed, the Play listing's graphics are managed by hand in the
|
||||
# console. Pass images:true once the assets qualify.
|
||||
upload_images = options[:images].to_s == "true"
|
||||
|
||||
supply(
|
||||
skip_upload_aab: true,
|
||||
skip_upload_apk: true,
|
||||
skip_upload_metadata: false,
|
||||
skip_upload_changelogs: true,
|
||||
skip_upload_images: !upload_images,
|
||||
skip_upload_screenshots: !upload_images,
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -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,22 @@
|
||||
|
||||
// One reviewable surface; the dashboard issue lists everything pending.
|
||||
dependencyDashboard: true,
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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,
|
||||
prHourlyLimit: 0,
|
||||
@@ -25,11 +58,28 @@
|
||||
|
||||
// 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: [
|
||||
// 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 +102,18 @@
|
||||
],
|
||||
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.
|
||||
// "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", "Pending", "Confidence"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user