Forgejo's API returns head_sha=null in workflow run objects; the correct
field is commit_sha. The skip-check always got None, so every hourly
schedule triggered a full redeploy of the same commit.
Move Android Firebase instrumented tests out of deploy.yml into a new
firebase-tests.yml workflow that runs once per day (3 AM UTC) and only
when Firebase-relevant files changed in the last 24 hours. On failure,
the workflow automatically creates a Forgejo issue labelled "Ready" with
instructions to find the root cause and fix it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
- The hourly `deploy.yml` schedule re-deployed the same commit repeatedly because it always diffed `HEAD~1..HEAD` — once a commit touching `lib/`/`pubspec.*` became HEAD, every hourly tick would detect "android changes" and deploy again.
- Fix: at the start of the `check-changes` job, query the Forgejo workflow runs API for the last successful `deploy.yml` run. If its `head_sha` matches current HEAD, output `android=false` / `linux=false` immediately, skipping all downstream jobs.
- `workflow_dispatch` bypasses this check (always deploys), matching the existing behaviour.
## Test plan
- [ ] Verify the `check-changes` job exits early on the next scheduled run after a successful deploy of the same commit
- [ ] Verify a new commit still triggers deployment normally
- [ ] Verify `workflow_dispatch` still deploys unconditionally
🤖 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/265
Adds a Renovate() Dagger function using the forgejo platform and a
.forgejo/workflows/renovate.yml workflow triggered at 06:00 UTC daily.
Uses RENOVATE_FORGEJO_TOKEN secret; no dedicated Renovate service account needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds renovate.json to enable automated dependency updates for
pub (pubspec.yaml), Dockerfile, and Forgejo Actions workflows.
The github-actions manager fileMatch is extended to cover
.forgejo/workflows/ in addition to the default .github/ path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce androidBase() and firebaseBase() helpers that wrap setup() with
the Gradle named-cache volume, mirroring the pattern already used in
BuildAndroidDebugApks(). Use these in BuildAndroidRelease(), setupKeystore(),
and BuildAndroidDebugApks() so Gradle dependencies survive Dagger
execution-cache misses instead of being re-downloaded on every source change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
During _load(), check whether a password exists in secure storage and track the result
in _hasStoredPassword. The password field validator now requires user input when no
password is stored, so _tryConnection() fails fast at form validation instead of
throwing an unhandled StateError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Track a heartbeat timestamp in ~/.sharedinbox-agent-heartbeat at the
start of each _run_loop() invocation so we can tell when it last ran.
- Add `agent_loop.py monitor` subcommand that exits 1 with a WARNING
message if the heartbeat is missing, corrupted, or older than 2 hours.
- Add .forgejo/workflows/monitor.yml scheduled workflow that runs the
monitor check every 2 hours on the self-hosted runner; a CI failure
serves as the warning when the loop is stalled.
- Add 7 unit tests covering all monitor / heartbeat scenarios.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a catch-up PR merge fails (PR stays open after the merge command), the loop sets the issue to State/Question and comments on it. But on the next cron tick the same PR is still open with passing CI, so it tries again — spamming the issue with identical comments every minute.
Fix: before attempting a catch-up merge, fetch the issue's current labels via `_get_issue_labels()`. If `State/Question` is already set, skip the PR entirely.
Closes#239
Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/242
fgj is in the nix store but was not included in the PATH glob loop,
causing `FileNotFoundError: 'fgj'` on every cron run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issues labelled State/ToPlan are now picked up by a dedicated planning
agent before any implementation happens. The agent posts a plan as an
issue comment, then the loop transitions the label to State/Planned and
leaves a resume command in a follow-up comment. A human reviews the plan
and manually promotes the issue to State/Ready to trigger implementation.
Planning agents run at higher priority than Ready issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forgejo reports deploy.yml (scheduled/dispatch) runs with event=push
and prettyref=main, identical to ci.yml push runs. The event-only
filter was insufficient — adding workflow_id == "ci.yml" prevents
deploy.yml runs from blocking or triggering false CI fix agents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_latest_main_ci_run() was using event != pull_request which still
matched deploy.yml schedule runs when their prettyref == "main",
blocking the loop from picking up new issues.
_latest_ci_run_for_branch() had the same issue: the else branch matched
any non-pull_request event including schedule runs.
Both functions now explicitly filter for event == "push" only.
Tests updated: rename _latest_ci_run → _latest_main_ci_run, mock
_open_issue_prs to prevent real API calls in unit tests, and update
_find_pr_for_branch side_effect to reflect the upstream post-merge
PR-still-open verification check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Play Store AAB upload was failing with httplib2.error.RedirectMissingLocation
when Google's API returned a redirect during the resumable upload initiation.
Switched from google-api-python-client (which uses httplib2 internally) to
pure requests-based AuthorizedSession, which handles redirects correctly.
Closes#198
The previous tests patched google_auth_httplib2 and googleapiclient which
no longer exist in the new implementation. Rewrite to mock AuthorizedSession
and _upload_aab_resumable, covering the same scenarios: happy path, retry
on transient errors, backoff delays, and exhausted attempts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
httplib2 treats 308 Resume Incomplete responses (used by Google's
resumable upload API) as redirects and raises RedirectMissingLocation
when the response lacks a Location header. Switch to
google.auth.transport.requests.AuthorizedSession + direct HTTP calls
so the upload uses the requests library, which handles 308 correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 3-attempt retry loop around the resumable AAB upload that catches
httplib2.error.RedirectMissingLocation (a transient network error) and
retries with exponential backoff (10s, 20s). A fresh MediaFileUpload is
created on each attempt because resumable upload objects cannot be reused
after failure. Also adds TestUploadRetry covering the retry path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap the resumable bundle upload in a loop of up to _MAX_UPLOAD_ATTEMPTS (3)
attempts. On httplib2.error.RedirectMissingLocation, recreate MediaFileUpload
(resumable uploads cannot reuse the same object) and wait 10 s / 20 s before
retrying. After all attempts are exhausted, raise RuntimeError chained to the
last exception. Add tests covering the retry path, backoff delays, fresh
MediaFileUpload on each attempt, and exhaustion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch deploy_playstore.py from requests/AuthorizedSession to the
googleapiclient.discovery client with google-auth-httplib2, so that
AuthorizedHttp(timeout=300) enforces a hard socket timeout on all
requests and num_retries=3 on every .execute() call enables automatic
retries for transient failures.
Update flake.nix and ci/main.go to install the new dependencies
(google-api-python-client, google-auth-httplib2, httplib2) instead of
the old google-auth + requests pair.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>