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>
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>
The Forgejo workflow_runs API has no head_branch field. For pull_request
events the branch lives in event_payload["pull_request"]["head"]["ref"];
for push events it is in prettyref. The old code used run.get("head_branch")
which always returned None, causing _latest_ci_run_for_branch to never find
the run and the loop to declare "no CI run after 15 min" and set the issue to
State/Question — even when CI had already passed.
Also fixes a pre-existing test mock that was missing the session_name kwarg.
Adds TestLatestCiRunForBranch covering both event types and the regression.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Dagger container running generate_build_history.py may not always
reach the deployment server (network constraints on the Dagger engine).
Rather than aborting the entire publish-website pipeline, log the SSH
verbose output (already added in the previous debug commit) and return
an empty file list so Hugo still builds and rsync still deploys the
site — just without updated build-history pages.
This unblocks the cron deploy that has been failing since c259d2da.
Temporary: print verbose SSH output on failure to identify why the
connection fails from inside the dagger container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All other ssh/scp calls in the dagger module use explicit -i /root/.ssh/id_ed25519.
This one was missing it, causing exit 255 inside the dagger container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- agent_loop.py: create log dir with mode 0700 and enforce it on
existing dirs; open log files with mode 0600; chmod state file
to 0600 after every write. Prevents other local processes from
reading agent output (which may contain credential paths) or
tampering with the state file's pid field.
- ci/main.go (TestAndroidFirebase): replace
echo "$FIREBASE_SA_KEY" > /tmp/key.json
with bash process substitution
--key-file=<(echo "$FIREBASE_SA_KEY")
The key is now passed via a file descriptor — it never touches
disk, so it cannot be stranded by a failed gcloud auth call or
snapshotted into the Dagger layer cache.
- ci.yml / deploy.yml: add "Cleanup TLS credentials" step
(if: always()) at the end of every job that calls
setup_dagger_remote.sh. Removes /tmp/dagger-tls,
/tmp/stunnel-dagger.conf, /tmp/stunnel.pid from the self-hosted
runner after each job, so client certs do not accumulate between
job runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Only pick up issues created by guettli, guettlibot, or guettlibot2
to prevent the loop from acting on external/bot issues.
- Post an explanatory comment on the issue whenever the loop sets
State/Question (agent killed, no CI run, no push detected), so the
reason is visible without digging through cron logs. Closes#158.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- agent_loop.py: agents now create an `issue-N-fix` branch and open a PR;
the loop discovers the PR via `fgj pr list`, tracks its CI run, squash-merges
on green, and falls back to the global-CI path if no PR exists (backward compat).
Adds `_find_pr_for_branch`, `_latest_ci_run_for_branch`, `_merge_pr` helpers.
- .forgejo/workflows/ci.yml: strip to the single fast `check` job only
(removes build-linux, deploy-playstore, publish-website).
- .forgejo/workflows/deploy.yml (new, replaces android-emulator-tests.yml):
scheduled hourly + workflow_dispatch; runs firebase tests, Play Store deploy,
Linux build/deploy, website publish; on completion sets CI/Full-Pass or
CI/Full-Fail label on the repo's DEPLOY_HEALTH_ISSUE tracking issue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If the agent exits immediately (e.g. rate-limit), the loop was closing the
pending issue against the *previous* CI run, which was still green.
Fix: record the latest CI run ID when an issue agent starts. If the run ID
hasn't changed when the agent exits, the agent pushed nothing → set
State/Question instead of closing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Capture gcloud auth stderr separately and fail on unexpected output;
ignore the two known informational lines ("Activated service account
credentials for: [...]" and "Updated property [core/project].") while
keeping a strict "fail if unknown stderr" check for anything else.
- Replace the narrow pattern grep (non-retryable error|infrastructure_failure|
test execution failed) with a broad whole-word case-insensitive grep for
'error', so any infrastructure or Firebase error in the output causes CI
failure.
- Verify that the number of device result rows in the result table matches
the expected device count (1), so a silent test-run failure cannot slip
through.
- Add scripts/test_firebase_check.sh with 18 unit tests for the three new
bash patterns (auth stderr filter, error-word detection, device count).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add -qq to apt-get update/install in Dagger toolchain to suppress
verbose package-list output (hundreds of lines on cold cache)
- Wrap sdkmanager in silent-on-success pattern — only shows output
on failure, like the build_runner and flutter pub get steps
- Set debug = warning in stunnel config to suppress LOG5 (info/notice)
startup lines while keeping LOG4 (warning) and above
- Add org.gradle.welcome=never to android/gradle.properties to
suppress the "Welcome to Gradle N.NN!" banner
- Filter SKIPPED Gradle tasks, Gradle Daemon startup messages, and
gcloud support-page promo lines in run_firebase_test.sh
Errors and warnings are preserved in all cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`tea api` exits 0 even on 401 responses, so `_close_issue` and
`_set_labels` appeared to succeed but did nothing. Issues were never
actually closed, causing them to be picked up again every cron tick.
Switch all write operations (close issue, set labels) and issue-list
reads to `fgj`, which has proper authentication. Keep `tea api` only
for CI run fetches where `fgj` times out (504). Add ~/go/bin to the
cron PATH so fgj is found.
Also add an error check in `_tea_get` for API-level error responses,
and strip State/InProgress when closing an issue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously issue agents were instructed to close the issue via prompt text
immediately after pushing. If CI then failed, the issue was already closed.
Now the loop tracks a pending_issue across cron ticks:
- When an agent finishes (issue or ci-fix), the issue number is extracted
from state before it is cleared.
- If CI is still running, a "pending-ci" state preserves the issue number.
- If CI fails, the ci-fix agent is started with the issue number in state
so it survives the fix cycle.
- Once CI passes, _close_issue() is called from Python — never by the agent.
The agent prompt no longer instructs the agent to close the issue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `---------------------- Starting YYYY-MM-DD HH:MMZ` header at each run
- Remove `[agent_loop]` prefix from all output lines
- Show full Codeberg URL for CI runs instead of bare run ID
- Show full issue URL and title when referencing issues
- Store issue_title in state file so "still running" messages include the title
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
State/Ready → State/InProgress is already set by agent_loop.py before
the agent starts. Update AGENTS.md to reflect that agents invoked via
the loop must not set InProgress themselves (only manual workflows need
to). Also fix TestMain tests that called main() directly, which caused
argparse to consume sys.argv; they now call _run_loop() instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add scripts/run_firebase_test.sh that strips ANSI codes and removes
UP-TO-DATE task lines, libsqlite warnings, Gradle deprecation notices
and other high-volume noise before it hits the CI log.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Filter flutter pub get package-listing lines (^[+~><] ) in pubGetLayer
- Filter build_runner compilation-progress lines (^\[) in setup() and CheckMocks()
- Add -q to git commit in CheckMocks to suppress "460 files changed" stats
- Wrap flutter test in Coverage, TestBackend, TestIntegration, TestSyncReliability
to show only the summary line on success and full output on failure
- Apply same build_runner filter to scripts/check_mocks_fresh.sh for local runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Connection drops consistently at ~50s suggest NAT/firewall idle timeout.
Keepalive probes every 10s on the remote side prevent the RST.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.daggerignore no longer needs to exclude $HOME dirs (fvm/, go/, .pub-cache/,
.claude/, snap/, etc.) since the project root is now sharedinbox/, not $HOME.
agent_loop.py: replace hardcoded /home/si with Path.home().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without the response body we can't tell why Google Play rejects the
upload. Logs the status code and first 500 bytes of the response for
both the init POST and the upload PUT on each failed attempt. Also
moves the init call inside the try/except so init failures are retried.
The resumable upload URL returned by Google Play is session-specific and
expires after a failed attempt. Retrying with the same URL always fails.
Also broadens the caught exception from HTTPError to RequestException so
timeouts and connection errors are retried too.
Add TestMain class covering the main() flow: asserts that _set_labels
is called with State/InProgress (and State/Ready removed) strictly
before _start_agent, and that no labels or agents are touched when
there are no ready issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the tmux-based agent launcher with a direct subprocess.Popen
call. Claude sessions can't be attached to anyway, so the tmux layer
added complexity with no benefit. State now tracks a PID instead of a
tmux session name; liveness is checked with os.kill(pid, 0).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The new Claude Code trust dialog appeared inside the tmux PTY despite -p
mode and stdout being piped, blocking the agent indefinitely. With
< /dev/null the dialog could never be answered.
Replace < /dev/null with printf '\n' | so the Enter keypress confirms the
default "Yes, I trust this folder" option. After that single newline stdin
reaches EOF, which -p mode ignores.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously claude was launched with -p (print mode) which produces no
visible TUI. Attaching to the session with `tmux attach -t issue-NNN`
showed a blank terminal. Removing -p makes Claude run its interactive
TUI inside the tmux pane, so the session is fully watchable.
Add scripts/test_agent_loop.py covering _start_agent command
construction and state file round-trips.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the insecure plaintext QR export/import flow with an
end-to-end-encrypted account-transfer mechanism:
- Receiver generates an ephemeral X25519 key pair (20-minute lifetime,
stored in the new share_keys DB table at schema v31) and displays it
as a QR code (sharedinbox.de:pubkey:v1:…).
- Sender scans the public-key QR, selects accounts (or auto-selects
when only one exists), encrypts them with ECIES (X25519-ECDH +
HKDF-SHA256 + AES-256-GCM) and displays an encrypted QR
(sharedinbox.de:encrypted-accounts:v1:…).
- Receiver scans the encrypted QR, decrypts, verifies the 20-minute
expiry and MAC authentication tag, then imports the accounts.
New screens: AccountReceiveScreen (/accounts/receive) and
AccountSendScreen (/accounts/send), accessible from the account-list
drawer and per-account popup menu respectively.
Remove the old insecure AccountExportScreen and AccountImportScreen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The builds page at /builds/ was empty because generate-build-history
only ran inside deploy-playstore; if that job failed early (e.g. Play
Store secrets not configured) the website was never updated, and the
build-linux job never triggered a website update at all.
Changes:
- generate_build_history.py: extend to cover Linux tarballs in addition
to Android APKs, capped at MAX_BUILDS_PER_PLATFORM (30) each
- Taskfile: add website-publish task (generate-build-history +
website-deploy), exclude *.tar.gz from rsync, update descriptions
- .forgejo/workflows/ci.yml: add publish-website job that waits for
both build-linux and deploy-playstore (using always() so it runs
even when deploy-playstore fails), then removes the duplicate
generate/deploy steps from deploy-playstore
- .github/workflows/ci.yml: add deploy job that deploys Linux build,
generates build history, builds Hugo site, and rsyncs to server
- .gitignore: ignore website/content/builds/_index.md (generated),
Python __pycache__, and widget test failure screenshots
- stalwart-dev/integration_ui_test.sh: use ${USER:-$(id -un)} for
robustness in environments where USER is unset
- scripts/test_generate_build_history.py: unit tests for parse_builds
and render_entries covering both platforms
Generated content (builds/_index.md and per-day pages) is not tracked
in git; it is produced at CI time and rsynced to the server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without `< /dev/null`, claude detects the tmux PTY as stdin and blocks
waiting for user input that never arrives (the PTY never sends EOF).
The 3-second stdin-timeout only fires for pipe stdin, not TTY stdin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace bare subprocess.Popen with `tmux new-session -d` so each agent
runs in a detached tmux session that inherits the tmux server's environment
(including ANTHROPIC_API_KEY / keychain access, which cron's minimal env
lacks — the root cause of intermittent empty log files).
- Track agents by tmux session name instead of PID; age is derived from the
state-file `started_at` timestamp rather than /proc/<pid>/stat.
- `_kill_agent` terminates via `tmux kill-session`; backward compat preserved
for old state files that stored a `pid`.
- Operators can now `tmux attach -t issue-<N>` to watch live output, or
`claude --resume issue-<N>` to continue the conversation afterward.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Export account screen (QR code + copy-to-clipboard) and Import
account screen (paste JSON code) so users can transfer IMAP/JMAP
account configuration to another device without re-entering every field.
- Account list popup: "Export account" opens a QR code with a password
warning and a copy-code button.
- Add Account screen: "Import account" button opens the import flow
where pasting the exported JSON pre-fills the account and one tap
saves it with a fresh generated ID.
- New routes: /accounts/:id/export and /accounts/import.
- Widget tests cover export display, import parsing, validation,
and the happy-path save-and-navigate flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cron runs with a minimal environment that doesn't include ~/.nix-profile/bin,
causing every invocation to crash with FileNotFoundError on 'tea'.
Closes#93