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
|
# 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,
|
# 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
|
# trigger. Ordinary merges (no version bump) fall through `detect` and do
|
||||||
# nothing.
|
# 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
|
# 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,
|
# 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
|
# without building an APK or creating a release. Used for key rotation / repo
|
||||||
@@ -174,6 +180,33 @@ jobs:
|
|||||||
if: env.IS_RELEASE == 'true'
|
if: env.IS_RELEASE == 'true'
|
||||||
run: ./gradlew assembleRelease
|
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
|
- name: Setup F-Droid Server Tools
|
||||||
run: |
|
run: |
|
||||||
SUDO=""
|
SUDO=""
|
||||||
@@ -412,20 +445,31 @@ jobs:
|
|||||||
"prerelease": False,
|
"prerelease": False,
|
||||||
}))
|
}))
|
||||||
PY
|
PY
|
||||||
# Upsert (re-run safe). POST also creates the tag at target_commitish
|
# Create (or update) the release. Codeberg 500s on a POST/GET against a
|
||||||
# if the push mirror hasn't synced it yet.
|
# tag it has only just received — the release request outruns the
|
||||||
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
# indexing of the ref we pushed a moment ago — so a single attempt kept
|
||||||
if [ -n "$ID" ]; then
|
# failing and skipping the mirror even though the very same call
|
||||||
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
# 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/$EXIST"
|
||||||
|
ID="$EXIST"; break
|
||||||
|
fi
|
||||||
|
CODE=$(curl -s -o cb-response.json -w "%{http_code}" -X POST \
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
-d @cb-payload.json "$API/releases/$ID"
|
-d @cb-payload.json "$API/releases")
|
||||||
else
|
echo "release POST attempt $attempt HTTP $CODE"
|
||||||
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)
|
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
||||||
fi
|
[ -n "$ID" ] && break
|
||||||
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id." >&2; exit 1; fi
|
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.
|
# Attach APK + checksum, replacing any prior asset of the same name.
|
||||||
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
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"
|
"$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n"
|
||||||
done
|
done
|
||||||
echo "Published $TAG to Codeberg."
|
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"]'
|
RENOVATE_REPOSITORIES: '["makiolaj/calendula"]'
|
||||||
# Commits/PRs authored as the bot, not a real maintainer.
|
# Commits/PRs authored as the bot, not a real maintainer.
|
||||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <renovate@jeanlucmakiola.de>'
|
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
|
LOG_LEVEL: info
|
||||||
|
|||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -56,5 +56,16 @@ Thumbs.db
|
|||||||
# KSP
|
# KSP
|
||||||
.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 Code
|
||||||
/CLAUDE.md
|
/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.Language
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.Palette
|
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.Translate
|
||||||
import androidx.compose.material.icons.filled.Tune
|
import androidx.compose.material.icons.filled.Tune
|
||||||
import androidx.compose.material.icons.filled.UploadFile
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
@@ -408,7 +409,9 @@ private fun LanguageRow(position: Position) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AboutCard() {
|
private fun AboutCard() {
|
||||||
// The card layout lives in floret-kit (components.AboutCard); Calendula
|
// 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(
|
AboutCard(
|
||||||
logo = { AppLogo() },
|
logo = { AppLogo() },
|
||||||
appName = stringResource(R.string.app_name),
|
appName = stringResource(R.string.app_name),
|
||||||
@@ -424,6 +427,11 @@ private fun AboutCard() {
|
|||||||
label = stringResource(R.string.settings_license),
|
label = stringResource(R.string.settings_license),
|
||||||
url = stringResource(R.string.about_license_url),
|
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(
|
highlightLink = AboutLink(
|
||||||
icon = Icons.Default.Favorite,
|
icon = Icons.Default.Favorite,
|
||||||
|
|||||||
@@ -53,11 +53,11 @@
|
|||||||
<string name="event_share_failed">تعذّر مشاركة هذا الحدث.</string>
|
<string name="event_share_failed">تعذّر مشاركة هذا الحدث.</string>
|
||||||
<string name="event_delete_title">حذف الحدث؟</string>
|
<string name="event_delete_title">حذف الحدث؟</string>
|
||||||
<string name="event_delete_body">الحدث أُزيل من تقويمك ومن كل جهاز تتم مزامنته معه.</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_occurrence">فقط هذا الحدث</string>
|
||||||
<string name="event_delete_option_following">هذا وجميع الأحداث اللاحقة</string>
|
<string name="event_delete_option_following">هذا وجميع الأحداث اللاحقة</string>
|
||||||
<string name="event_delete_option_series">جميع الأحداث في السلسلة</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_failed">تعذّر حذف الحدث</string>
|
||||||
<string name="event_delete_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
<string name="event_delete_write_denied">Calendula يحتاج إلى صلاحية للكتابة لحذف الأحداث</string>
|
||||||
<string name="dialog_cancel">إلغاء</string>
|
<string name="dialog_cancel">إلغاء</string>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<string name="event_edit_conflict_discard_hint">الحدث يبقى كما هو الآن</string>
|
<string name="event_edit_conflict_discard_hint">الحدث يبقى كما هو الآن</string>
|
||||||
<string name="import_reminder_prompt_title">هل تريد تطبيق تذكيرك الافتراضي؟</string>
|
<string name="import_reminder_prompt_title">هل تريد تطبيق تذكيرك الافتراضي؟</string>
|
||||||
<string name="event_edit_gone_body">هذا الحدث حُذف في هذه الأثناء، على سبيل المثال على جهاز آخر. لم يعد من الممكن حفظ تغييراتك.</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_public">علني</string>
|
||||||
<string name="event_access_default">الافتراضي</string>
|
<string name="event_access_default">الافتراضي</string>
|
||||||
<string name="event_availability_busy">مشغول</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_until">%1$s حتى %2$s</string>
|
||||||
<string name="recurrence_with_count">%1$s، %2$d مرّات</string>
|
<string name="recurrence_with_count">%1$s، %2$d مرّات</string>
|
||||||
<string name="import_reminder_prompt_apply">طَبِّق الافتراضي</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="recurrence_on_days">%1$s في %2$s</string>
|
||||||
<string name="event_attendee_unknown">—</string>
|
<string name="event_attendee_unknown">—</string>
|
||||||
<string name="event_attendee_accepted">تم القُبول</string>
|
<string name="event_attendee_accepted">تم القُبول</string>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
<string name="event_status_cancelled">ملغي</string>
|
<string name="event_status_cancelled">ملغي</string>
|
||||||
<string name="event_availability_free">متاح</string>
|
<string name="event_availability_free">متاح</string>
|
||||||
<string name="event_access_private">خاص</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_organizer">المُنظِّم</string>
|
||||||
<string name="event_attendee_optional">اختياري</string>
|
<string name="event_attendee_optional">اختياري</string>
|
||||||
<string name="reminder_default">التذكير الافتراضي</string>
|
<string name="reminder_default">التذكير الافتراضي</string>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
<string name="reminder_onboarding_body">Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
|
<string name="reminder_onboarding_body">Android لا يعرض تذكيرات الأحداث من نفسه — بل يجب أن يقوم تطبيق تقويم بذلك. دع Calendula يتولى هذه المهمة.</string>
|
||||||
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string>
|
<string name="reminder_benefit_delivery_body">كل تذكير لأحداثك يصِل كإشعار، في الوقت المحدد تمامًا.</string>
|
||||||
<string name="reminder_benefit_duplicates_title">هل تستخدم تطبيق تقويم ثانٍ؟</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_benefit_reversible_title">يمكنك تغييره في أي وقت</string>
|
||||||
<string name="reminder_onboarding_enable_button">شَغِّل التذكيرات</string>
|
<string name="reminder_onboarding_enable_button">شَغِّل التذكيرات</string>
|
||||||
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
<string name="reminder_onboarding_skip_button">ليس الآن</string>
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
<string name="search_hint">البحث عن الأحداث</string>
|
<string name="search_hint">البحث عن الأحداث</string>
|
||||||
<string name="search_clear">مسح</string>
|
<string name="search_clear">مسح</string>
|
||||||
<string name="agenda_no_more_today">لا مزيد من الأحداث اليوم</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_idle_hint">ابحث عن أحداثك بالعنوان، الموقع أو الملاحظات.</string>
|
||||||
<string name="search_back">الرجوع</string>
|
<string name="search_back">الرجوع</string>
|
||||||
<string name="search_action">البحث</string>
|
<string name="search_action">البحث</string>
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
<string name="back">رجوع</string>
|
<string name="back">رجوع</string>
|
||||||
<string name="settings_section_appearance">المظهر</string>
|
<string name="settings_section_appearance">المظهر</string>
|
||||||
<string name="settings_dynamic_color">اللون الديناميكي</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_default_view">طريقة العرض الافتراضية</string>
|
||||||
<string name="settings_soften_colors">ألوان تقويم ناعمة</string>
|
<string name="settings_soften_colors">ألوان تقويم ناعمة</string>
|
||||||
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
<string name="settings_soften_colors_summary">خفف ألوان التقويم والأحداث لتتناسب مع الثيم. قم بإيقافه لإظهار الألوان الخام من مصدر التقويم.</string>
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
<string name="settings_font_choose_file">اختر ملفًا…</string>
|
<string name="settings_font_choose_file">اختر ملفًا…</string>
|
||||||
<string name="settings_font_custom_selected">خط مخصص</string>
|
<string name="settings_font_custom_selected">خط مخصص</string>
|
||||||
<string name="settings_font_import_failed">تعذّر قراءة هذا الملف كخط</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_week_start_auto">تلقائي</string>
|
||||||
<string name="settings_time_format_auto">تلقائي</string>
|
<string name="settings_time_format_auto">تلقائي</string>
|
||||||
<string name="settings_time_format">تنسيق الوقت</string>
|
<string name="settings_time_format">تنسيق الوقت</string>
|
||||||
@@ -243,4 +243,58 @@
|
|||||||
<string name="settings_past_events_show">إظهار</string>
|
<string name="settings_past_events_show">إظهار</string>
|
||||||
<string name="settings_past_events_hide">إخفاء</string>
|
<string name="settings_past_events_hide">إخفاء</string>
|
||||||
<string name="settings_agenda_header">جدول</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>
|
</resources>
|
||||||
|
|||||||
@@ -354,7 +354,7 @@
|
|||||||
<string name="settings_translate">Pomóż w tłumaczeniu</string>
|
<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_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_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_event_form_subtitle">Domyślne pola dla nowych wydarzeń</string>
|
||||||
<string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
|
<string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
|
||||||
<string name="settings_special_dates_subtitle">Urodziny i rocznice kontaktów</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="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_birthday">Urodziny</string>
|
||||||
<string name="special_dates_calendar_anniversary">Rocznice</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>
|
</resources>
|
||||||
|
|||||||
@@ -462,6 +462,7 @@
|
|||||||
<string name="settings_license_value">MIT</string>
|
<string name="settings_license_value">MIT</string>
|
||||||
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
||||||
<string name="settings_about_source">Source</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_support">Support development</string>
|
||||||
<string name="settings_about_version">Version %1$s</string>
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
<string name="settings_about_logo_desc">Calendula app icon</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_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_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_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>
|
<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
|
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`
|
(both best-effort). Ordinary merges with no version bump fall through `detect`
|
||||||
and do nothing.
|
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
|
### 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
|
`CODEBERG_RELEASE_TOKEN` secret (Codeberg access token, `write:repository` scope) added
|
||||||
to Gitea Actions.
|
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
|
### Manual re-sign / recovery
|
||||||
|
|
||||||
A manual `workflow_dispatch` of the release workflow runs a **re-sign-only**
|
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. |
|
| `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. |
|
| `GITHUB_TOKEN` | Provided by Gitea Actions; used to create the release + attach assets. |
|
||||||
| `CODEBERG_RELEASE_TOKEN` | Codeberg access token (`write:repository` scope) — creates the mirrored Codeberg release + uploads the APK/checksum. Best-effort; if unset the Codeberg step skips. |
|
| `CODEBERG_RELEASE_TOKEN` | Codeberg access token (`write:repository` scope) — 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**
|
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
|
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",
|
":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
|
// No automerge: a dependency bump goes through the same review (and, for
|
||||||
// anything touching the build, the same on-device check) as a feature
|
// anything touching the build, the same on-device check) as a feature
|
||||||
// before it can ride a release — see docs/RELEASING.md and the
|
// 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.
|
// One reviewable surface; the dashboard issue lists everything pending.
|
||||||
dependencyDashboard: true,
|
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"],
|
labels: ["dependencies"],
|
||||||
prConcurrentLimit: 5,
|
prConcurrentLimit: 5,
|
||||||
prHourlyLimit: 0,
|
prHourlyLimit: 0,
|
||||||
@@ -25,11 +58,28 @@
|
|||||||
|
|
||||||
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
// Gitea Actions workflows live under .gitea/workflows, not .github — extend
|
||||||
// the github-actions manager (same syntax) to watch them too.
|
// 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": {
|
"github-actions": {
|
||||||
fileMatch: ["^\\.gitea/workflows/[^/]+\\.ya?ml$"],
|
managerFilePatterns: ["/^\\.gitea/workflows/[^/]+\\.ya?ml$/"],
|
||||||
},
|
},
|
||||||
|
|
||||||
packageRules: [
|
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
|
// material3 is deliberately pinned to the 1.5 *alpha* line for the
|
||||||
// Expressive APIs (see gradle/libs.versions.toml). Follow the alpha train
|
// 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
|
// but keep it in its own PR, reviewed in isolation; revisit the pin when
|
||||||
@@ -52,5 +102,18 @@
|
|||||||
],
|
],
|
||||||
groupName: "test dependencies",
|
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