Closes#415
## Summary
- Adds missing `timeout-minutes` to `ci.yml` (`check` job, 60 min) and `windows-nightly.yml` (90 min, ready for when the Windows runner is registered)
- Wraps `ssh-keyscan` and `ssh -f -N -L` tunnel creation in `setup_dagger_remote.sh` with `timeout 30`; emits a `::warning::` annotation when either takes more than 10 s
- Adds `timeout --kill-after=10 <N>` to all bare `dagger call` invocations in `Taskfile.yml`: 600 s for test/query tasks, 1800 s for build/deploy tasks, 60 s for `ci-graph`; `stalwart` and `check-dagger` (already protected) left untouched
- Adds `timeout --kill-after=10 2400` per attempt in `run_firebase_test.sh`; emits `::warning::` on exit 124 instead of silently retrying
## Test plan
- CI passes on this PR (the `check` job now has `timeout-minutes: 60` and will self-enforce)
- All `dagger call` lines in `Taskfile.yml` now have a `timeout` prefix (visible in the diff)
- `setup_dagger_remote.sh` logic is unchanged — only the two network calls are wrapped
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/432
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [gradle](https://gradle.org) ([source](https://github.com/gradle/gradle)) | major | `8.14.5` → `9.5.1` |
---
> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/276) for more information.
> ❗ **Important**
>
> Release Notes retrieval for this PR were skipped because no github.com credentials were available.
> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMTIuNCIsInVwZGF0ZWRJblZlciI6IjQzLjIxMi40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/438
## Summary
- `BuildAndroidRelease` in `ci/main.go` intentionally builds the AAB without setting up the keystore — the unsigned AAB is later stamped with `StampAndroidVersionCode` and re-signed by `SignAndroidBundle` via jarsigner.
- The old `signingConfigs.create("release")` block in `android/app/build.gradle.kts` called `error("ANDROID_KEYSTORE_PATH is not set")` at Gradle _configuration_ time, which fired even when the keystore wasn't needed for the build step.
- Fix: guard the `signingConfigs` block and the `signingConfig` assignment in the release build type behind a null-check on `ANDROID_KEYSTORE_PATH`. When the env var is absent (unsigned build path), Gradle skips the signing config entirely; when it is present (e.g. `BuildAndroidApk` via `setupKeystore`), the config is created and applied as before.
## Test plan
- Trigger `deploy.yml` via `workflow_dispatch` and verify the `Build & Deploy to Play Store` job no longer fails at step 4 with "ANDROID_KEYSTORE_PATH is not set"
- Verify `BuildAndroidApk` (which calls `setupKeystore`) still produces a correctly signed APK
Closes#439🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/440
Keystore is decoded into /dev/shm (tmpfs, RAM-only) during the build
and cleaned up on exit — never written to physical disk. ANDROID_KEYSTORE_PATH
is now required with no fallback; missing it fails loudly. Dagger CI path
updated to write to /tmp and set ANDROID_KEYSTORE_PATH accordingly.
Also fix check_ci_images.sh: filter out incomplete image tags ending in ':'
that arise from dynamic From("image:"+variable) concatenations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
- Downgrades `.fvmrc` from Flutter `3.44.1` back to `3.44.0` — `ghcr.io/cirruslabs/flutter:3.44.1` does not exist on GHCR so every Dagger-based deploy job fails with "not found"
- Extends `scripts/check_ci_images.sh` to also validate the Flutter image derived from `.fvmrc` (previously only literal `From("...")` calls in `ci/main.go` were checked, so Renovate bumps to non-existent images went undetected)
- Updates `.pre-commit-config.yaml` to trigger the `ci-image-exists` hook on `.fvmrc` changes as well as `ci/main.go`
## Root cause
Recent run logs showed:
```
! ghcr.io/cirruslabs/flutter:3.44.1: not found
Error: failed to resolve image "ghcr.io/cirruslabs/flutter:3.44.1"
```
Renovate bumped Flutter to 3.44.1 (#411) but cirruslabs has not published that image — the latest available is `3.44.0`. Same root cause as #409, but the pre-commit guard only watched `ci/main.go`, not `.fvmrc`.
Closes#427
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/428
The job timeout was removed in the simplification of ci.yml but is
important to prevent CI jobs from running indefinitely if the Dagger
engine connection hangs instead of failing fast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## What
`ci/main.go` previously hardcoded the Flutter container image tag (`ghcr.io/cirruslabs/flutter:3.44.0`) separately from `.fvmrc` (`{ "flutter": "3.44.1" }`). These two values drifted, causing the deploy failure in #394.
## How
`New()` now accepts `ctx context.Context` and returns `(*Ci, error)`. It reads `.fvmrc` from the source directory, parses the `flutter` field, and stores it as `Ci.FlutterVersion`. `toolchain()` constructs the image tag as `"ghcr.io/cirruslabs/flutter:" + m.FlutterVersion`. `Graph()` also uses the live value instead of a stale literal.
Result: `.fvmrc` is the single source of truth. Bumping Flutter via Renovate or manually only requires editing `.fvmrc`; the Dagger pipeline picks up the new version automatically.
## Verification
- `gofmt -e ci/main.go` passes
- No schema changes; no `build_runner` run needed
Closes#396
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Co-authored-by: guettli <guettli@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/405
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [flutter](https://flutter.dev) ([source](https://github.com/flutter/flutter)) | patch | `3.44.0` → `3.44.1` |
---
> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/276) for more information.
> ❗ **Important**
>
> Release Notes retrieval for this PR were skipped because no github.com credentials were available.
> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMTEuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIxMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJhdXRvbWVyZ2UiLCJkZXBlbmRlbmNpZXMiXX0=-->
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/411
## Summary
- Adds `scripts/check_ci_images.sh`: extracts every `From("...")` image reference from `ci/main.go` and runs `skopeo inspect --no-creds` on each one (manifest-only, no layer pull, no daemon required)
- Adds `task check-ci-images` task in `Taskfile.yml` that runs the script
- Adds `ci-image-exists` hook to `.pre-commit-config.yaml` that fires only when `ci/main.go` is staged (using `files: ^ci/main\.go$` rather than `always_run`, to avoid a network round-trip on every unrelated commit)
- Adds `skopeo` to the Nix devShell so the tool is on PATH when the hook runs via `nix develop --command`
This catches a bad image tag (like `ghcr.io/cirruslabs/flutter:3.44.1` not yet published) at commit time, before the push reaches CI.
## Test plan
- Stage a change to `ci/main.go` bumping a `From("...")` tag to a non-existent version → hook rejects commit with NOT FOUND
- Stage a change with valid image tags → hook prints OK for each image and allows the commit
- Stage a change to any other file → `ci-image-exists` hook is skipped entirely
Closes#407
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/413
When zone errors bubble up through Dart's async machinery the stack
trace is in package:stack_trace chain format (with '===== asynchronous
gap =====' separators). Flutter's StackFrame parser asserts on those
lines. FlutterError.demangleStackTrace strips the chain format back to
a plain VM trace before Flutter tries to parse it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
runZonedGuarded's error handler runs in the parent zone, so calling
runApp there caused a Flutter zone mismatch with ensureInitialized.
Removed the async keyword from main (redundant with runZonedGuarded)
and replaced the zone error handler's runApp call with reportError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
- Reverts `.fvmrc` from `3.44.1` to `3.44.0`
- `ghcr.io/cirruslabs/flutter:3.44.1` returns "manifest unknown" — image does not exist on GHCR
- `ghcr.io/cirruslabs/flutter:3.44.0` is confirmed present — CI can pull the toolchain container again
Closes#408
## Test plan
- [x] `docker manifest inspect ghcr.io/cirruslabs/flutter:3.44.0` returns a valid manifest (verified locally)
- [x] `docker manifest inspect ghcr.io/cirruslabs/flutter:3.44.1` returns "manifest unknown" (confirmed root cause)
- [ ] CI pipeline should pass once the toolchain image resolves correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/409
## Summary
Fixes the persistent `Load key "/root/.ssh/id_ed25519": error in libcrypto` failures in the `deploy-apk` and `deploy-linux` CI jobs (and the `website` workflow SSH steps) that have been occurring on every deploy run since the jobs first started running after #369.
Closes#404
### Root cause (diagnosed from run #1516 log)
Two compounding problems were found:
1. **Stale Dagger cache** — The `tr -d \x27\r\x27` normalisation step added in #369 was shown as `CACHED` by Dagger on every subsequent run. Dagger caches by input-content hash; if the very first execution produced a corrupted key file, that broken cached layer is replayed forever.
2. **`.ssh/` directory permissions** — Dagger creates parent directories for secret mounts with 755 permissions. Mounting the raw key directly inside `/root/.ssh/` may cause Dagger to (re-)create that directory with 755 instead of the 700 that OpenSSH requires.
### Changes (`ci/main.go` — `Deployer` function only)
- **Explicit `.ssh` setup**: `mkdir -p /root/.ssh && chmod 700 /root/.ssh` runs before any Dagger secret mount.
- **Move raw-key mount out of `.ssh/`**: Secret mounted at `/tmp/id_ed25519.raw`.
- **Python3 normalisation instead of `tr`**: Handles CRLF, bare-CR, and missing trailing newline. Changing the command changes the Dagger cache key, forcing a fresh read of the current live secret.
## Test plan
- [ ] `deploy-apk` job completes without `error in libcrypto`
- [ ] `deploy-linux` job completes without `error in libcrypto`
- [ ] `publish-android` (Play Store) job continues to succeed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/406
## Summary
Fixes the crash reported in #397: `Scaffold.of() called with a context that does not contain a Scaffold.`
- `Scaffold.of(context)` was called in the `onPressed` of the bottom-nav menu `IconButton` using the widget's own `build` context. That context is the *parent* of the `Scaffold` being returned, so Flutter correctly throws.
- Fix: wrap the `IconButton` in a `Builder`, which provides a child `ctx` that is a proper descendant of the `Scaffold`. `Scaffold.of(ctx)` then resolves correctly.
## Test plan
- [ ] Run app with bottom menu position enabled, tap the hamburger icon — drawer opens without crashing.
- [ ] Run app with top menu position — no regression (bottom nav is not rendered).
Closes#397🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/403
## Summary
- Removes the `AlertDialog` popup that appeared when tapping delete in thread view
- Deletion now happens immediately, matching the behaviour of the single mail view
- The existing `UndoShell` widget already listens for new `UndoAction` pushes and shows a snack bar with an **Undo** button — no extra UI code needed
Closes#398
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/402
## Summary
- When deleting a mail from the single Mail View, \`pushAction()\` was called with \`unawaited\` before \`_navigateTo()\`. This meant the UndoShell snack bar fired *after* navigation had already started, showing the snack bar on the destination scaffold mid-transition — which prevented the snack bar's duration timer from starting correctly.
- Fixed by changing \`unawaited(pushAction(...))\` to \`await pushAction(...)\`. Since Riverpod fires \`ref.listen\` synchronously when state changes, the UndoShell now queues the snack bar on the current stable scaffold *before* \`_navigateTo()\` is called. The snack bar then naturally transfers to the destination scaffold and auto-dismisses after 5 seconds as intended.
Closes#399
## Test plan
- [x] All 338 unit/widget tests pass
- [ ] Manually delete a mail from single Mail View and verify the snack bar appears and auto-dismisses after ~5 seconds
- [ ] Verify the Undo button in the snack bar still works
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/401
Closes#373
## Summary
- **Schema v38**: two new columns on `user_preferences` — `prefetch_mode` (default `wifiOnly`) and `body_cache_limit_mb` (default 100 MB).
- **`BodyCacheService`**: queries for emails that have no cached body, fetches them newest-first in batches of 20, and evicts the oldest cached bodies when the configured size limit is exceeded.
- **Separate WorkManager task** (`si_bg_prefetch`): runs hourly with `NetworkType.unmetered` (Wi-Fi) or `NetworkType.connected` (any) depending on the user's choice. The task is cancelled when prefetch is disabled.
- **App startup**: reads the stored preference from the DB and re-registers the WorkManager task with the correct constraint.
- **Preferences screen**: radio group for prefetch mode (Wi-Fi only / Any network / Disabled) and a dropdown for cache size limit (50 / 100 / 200 / 500 MB).
## What is NOT downloaded
Binary attachments are never fetched — `getEmailBody()` stores only `textBody` and `htmlBody`. The cache size limit + per-run batch cap (20 emails) keep storage bounded even on large mailboxes.
## Test plan
- [x] `task analyze` — no issues
- [x] `task test` — all 492 tests pass (incl. updated migration_test.dart for v38)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/400
## Summary
- Wraps the \`dagger call\` in \`deploy-linux\`, \`publish-android\`, and \`deploy-apk\` Taskfile tasks with \`scripts/silent_on_success.sh\`
- On success: no Dagger output is printed (eliminates the verbose logs seen in deploy.yml CI runs)
- On failure: full Dagger output is replayed so errors remain visible
The project already uses \`scripts/silent_on_success.sh\` for other noisy commands (fvm, flutter pub get, build_runner, etc.) — this applies the same pattern to the three deploy tasks called from \`.forgejo/workflows/deploy.yml\`.
Closes#389
## Test plan
- [ ] Verify deploy CI run produces significantly less output on success
- [ ] Verify that on a failure, the full Dagger output is still printed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/390
## Summary
- When viewing an email and then deleting (or archiving/moving/snoozing) it, the app navigates to the next email in the thread list.
- `getEmailBody` fetches from the network on a cache miss, causing the hourglass / loading spinner the issue describes.
- `EmailDetailNotifier` now fires a background `getEmailBody` call for the next thread's `latestEmailId` as soon as the current email finishes loading.
- `getEmailBody` already caches results in the `EmailBodies` table with a 7-day TTL, so by the time the user triggers a navigation action the body is pre-warmed and renders instantly.
## What changed
`lib/di.dart` — `EmailDetailNotifier.build()` calls `_prefetchNextEmailBody` (fire-and-forget via `unawaited`) after loading the current email. The helper respects the `afterMailViewAction` user preference: if set to `showMailbox` it does nothing.
## Test plan
- [ ] Open an email, delete it — next email should appear without the spinner
- [ ] Verify the same for archive, move, and snooze actions
- [ ] Verify behaviour is unchanged when `afterMailViewAction` is set to `showMailbox`
- [ ] Verify the last email in the list still pops back to the mailbox list correctly
Closes#367
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/381
## Summary
Closes#377
- Adds a new `ImageTrustedSenders` Drift table (schema v37) that stores email addresses for which remote images are loaded automatically (per device, not per account)
- When the user taps "Load remote images", the sender's address is saved and a 3-second snackbar appears with a "Settings" hyperlink to undo the choice in preferences
- Both `EmailDetailScreen` and `ThreadDetailScreen` check the trusted senders list on open and auto-load images for known senders
- The Preferences screen gains a new "Trusted image senders" section listing all saved senders with individual remove buttons
## Test plan
- [x] `dart run build_runner build` regenerates `database.g.dart` cleanly (schema v37)
- [x] `flutter analyze` — no issues
- [x] Migration test updated: checks `image_trusted_senders` table exists after upgrade and fresh install
- [x] `FakeUserPreferencesRepository` updated with three new interface methods
- [x] All 490 unit + widget tests pass (1 pre-existing golden test failure unrelated to this change)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/378
## What changed
`searchAddresses` (used by the To/Cc/Bcc autocomplete) now runs two passes over the candidate email rows:
1. **Sent-folder rows first** — the mailboxes table is queried for mailboxes with `role='sent'`; any email row whose `mailboxPath` matches gets processed before inbox/other rows. Within this group addresses are ordered by `receivedAt` DESC as before.
2. **All other rows** — processed after sent rows, also by `receivedAt` DESC.
Within sent-folder rows, `toAddresses` and `ccJson` are checked before `fromJson` (the sender in a sent email is our own address, not a useful suggestion). For non-sent rows the original order (`fromJson`, `toAddresses`, `ccJson`) is kept.
This means: if you wrote to `info@foo.de` yesterday and received spam from `info@spam.de` today, typing "i" surfaces `info@foo.de` first.
## How verified
- All 492 unit tests pass (`task test`).
- Added a dedicated test `searchAddresses prioritises sent-folder addresses over newer received` that inserts an older sent email and a newer received email matching the same query prefix and asserts the sent-folder address is returned first.
Closes#375
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/380
Closes#372
## What changed
- **New widget** `lib/ui/widgets/email_headers_dialog.dart`: full-screen header browser that organises headers into collapsible groups:
- **Headers** — all standard headers (expanded by default)
- **List- Headers** — all `List-*` headers grouped together (expanded)
- **Received** — all `Received` headers, **collapsed by default**; shows the inter-hop duration between consecutive entries and highlights delays in colour (green < 30 s, orange < 5 min, red >= 5 min)
- **ARC- Headers** — all `ARC-*` headers (above X-, expanded)
- **X-Prefix Headers** — X- headers split by their second component (e.g. `X-Google-*` → "X-Google Headers"), sorted alphabetically, at the very bottom
- **`email_detail_screen.dart`**: `_showHeaders` now uses `EmailHeadersDialog`; `_showStructure` converted from `AlertDialog` to `Dialog.fullscreen()` — satisfying "Make popup windows full screen."
- **`scripts/check_coverage.dart`**: new widget file added to the `_excluded` set (UI widgets are covered by integration tests, not unit tests).
## Verified
`task check` passes (analyze: no issues, 491 unit tests pass, coverage >= 80 %).
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/374
## What
PR #356 (Renovate) was blocked with `renovate/artifacts` — \"Artifact file update failure\" — because `ci/go.sum` could not be updated automatically.
**Root cause**: `ci/main.go` imports `dagger/ci/internal/dagger` (generated by `dagger develop`, not committed to the repo). Without that generated package present, `go mod tidy` cannot resolve the full dependency graph, so Renovate's artifact update step always fails.
The actual OpenTelemetry version bump from PR #356 was already applied manually in PR #363.
## Fix
Adds a `packageRule` to `renovate.json` to disable the `gomod` manager for `ci/**`. Renovate will no longer open failing PRs for Go dependencies in the Dagger CI module; updates to `ci/go.mod` and `ci/go.sum` must be done manually (using `dagger develop && go mod tidy` inside `ci/`).
## Verification
- `renovate.json` validates against the Renovate schema.
- No Go or Drift schema changes; `task check` is unaffected.
Closes#368
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Co-authored-by: guettli <guettli@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/370
## Summary
Fixes three distinct failures from CI deploy run #1424 and concurrent website update failures.
- **Play Store job**: `pip install google-auth requests` fails on Ubuntu 24.04 with PEP 668. Fixed by using `python3 -m venv` for an isolated install.
- **SSH key error (APK, Linux, website jobs)**: All SSH/rsync steps fail with `Load key "/root/.ssh/id_ed25519": error in libcrypto` inside the Dagger Alpine 3.21 container. This is the first time these jobs actually ran (all previous deploy runs had every job skipped). Two fixes:
- `setup_dagger_remote.sh`: `export_secret` was appending an extra trailing newline to values (like SSH private keys) that already end with `\n`. Now only adds one when needed.
- `ci/main.go` `Deployer`: mounts the key at a `.raw` path, strips Windows-style CRLF endings with `tr -d '\r'`, then writes the normalised key to `id_ed25519`. CRLF bytes cause "error in libcrypto" in Alpine's LibreSSL-backed openssh.
## Test plan
- [ ] Deploy run triggers after merge; all three deploy jobs complete
- [ ] Play Store verification step passes
- [ ] SSH commands in Alpine load the key without `error in libcrypto`
Closes#366
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/369
Closes#361
Three bugs in the hourly deploy workflow's change-detection logic caused the Play Store to silently fall behind whenever a deploy failed or all-android jobs were skipped.
**Bug 1 (primary): commit_sha → head_sha**
Forgejo's API returns head_sha; commit_sha was always None. This meant LAST_DEPLOYED_SHA was always empty, so the diff fell back to HEAD~1..HEAD — only the single most recent commit was inspected. If android changes landed in an earlier commit, they were silently missed.
**Bug 2: Skipped runs counted as 'deployed'**
A workflow run where deploy-playstore was skipped (android=false) has status=success, so it was treated as a successful deploy. Now the code queries each run's job results and only trusts a run where the 'Build & Deploy to Play Store' job's own conclusion=success.
**Bug 3: Narrow fallback when SHA unknown**
When LAST_DEPLOYED_SHA could not be determined the workflow diffed HEAD~1..HEAD — potentially missing many commits. Now it defaults to android=true / linux=true (deploy everything) as the safe fallback.
Additional changes:
- ::error:: / ::warning:: / ::notice:: annotations so skip/failure reasons surface in the Actions UI.
- scripts/verify_playstore_deploy.py: new post-deploy check that queries the internal track and fails if the latest version code is more than 1 hour old. (Version codes are Unix timestamps set by ci/main.go's PublishAndroid.) Catches silent deploy failures the upload API did not reject.
- scripts/test_verify_playstore_deploy.py: 5 unit tests for the verify script (all pass).
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/364
Renovate only tracked the engine version in `ci/dagger.json`. This PR adds regex `customManagers` so Renovate also updates:
- `DAGGER_VERSION` in `.forgejo/Dockerfile`
- the nix flake reference (`github:dagger/nix/vX.Y.Z#dagger`) in `DAGGER.md`
All three now point to the same `dagger/dagger` GitHub releases datasource so they stay in sync via a single grouped PR.
Also bumps the stale `DAGGER.md` nix reference from `v0.11.4` to `v0.20.8` to match the current engine version.
Closes#358
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/365