Files
cc-ci/REVIEW.md
2026-05-27 10:41:29 +01:00

28 KiB
Raw Blame History

REVIEW — cc-ci Adversary (append-only)

This file is owned by the Adversary loop (§6.1). The Builder seeds this stub at bootstrap and does not edit it afterward. Adversary appends milestone/D-item verdicts (<id>: PASS @<ts> + evidence, or FAIL + a finding in BACKLOG.md ## Adversary findings), and may write ## VETO.

M0 — Foundations: PASS @2026-05-26T21:35Z

Verified cold (fresh shell, own clone /srv/cc-ci/cc-ci-adv, isolated host build dir /root/cc-ci-advverify, no reuse of Builder's /root/cc-ci).

Acceptance — "systemctl is-system-running healthy after a rebuild from the repo" + Builder's sops claim:

  • Repo rebuilds cc-ci: synced M0 commit deb4a0f (git-archive, no .git) to host, ran nixos-rebuild build --flake .#cc-ciBUILD EXIT 0, produced …-nixos-system-nixos-24.11.20250630.50ab793. Current HEAD also builds clean.
  • System health: systemctl is-system-runningrunning; systemctl --failed → 0 units.
  • sops decrypt: /run/secrets/test_secret present, mode 400 root:root, 41 bytes, value begins cc-c… (matches claimed generated cc-ci-m0-…). secrets/secrets.yaml is genuinely encrypted (2× ENC[…] + sops metadata block).
  • D6 leak probe (early): the decrypted plaintext value appears 0 times across all git history (git grep -F over git rev-list --all) and 0× in plaintext in secrets.yaml. No leak.

Note (not a finding; context for the M1 gate): the running system is already ahead of M0 — its closure includes docker, unit-swarm-init, and traefik units (traefik.yml, traefik-stack.yml, unit-traefik-deploy) that are not yet committed (HEAD ab839ae is swarm-only, no traefik). Expected mid-M1 churn, but the Traefik config must be committed to the repo before M1 is claimed or it fails D8 reproducibility — will check at the M1 gate.

M1 — Swarm + abra target: PASS @2026-05-26T22:20Z

Verified cold from own clone; deployed my own probe recipe via abra (not trusting the Builder's hand-test). Acceptance "a recipe deployed via abra is reachable over HTTPS at *.ci.commoninternet.net, then fully torn down leaving no volumes" + orchestrator's M1 checklist (ad).

  • (a) Real coop-cloud/traefik recipe (not hand-rolled): docker service lstraefik_…_app (traefik:v3.6.15) + …_socket-proxy (lscr.io socket-proxy) — the canonical recipe layout, deployed via abra (scripts/deploy-proxy.sh). modules/traefik.nix is deleted.
  • (b) Wildcard on web-secure + proxy overlay: static traefik.yml has web-secure: :443 (web→web-secure 301 redirect, verified live). File provider /etc/traefik/file-provider.yml: tls.certificates: [{certFile:/run/secrets/ssl_cert, keyFile:/run/secrets/ssl_key}]; swarm secrets …_ssl_cert_v1/…_ssl_key_v1 mounted (2909 B / 227 B = the pre-issued cert). My probe app advm1probe_…_app was attached to the proxy overlay.
  • E2E (cold deploy): abra app new custom-html -D advm1probe.ci.commoninternet.net (forced LETS_ENCRYPT_ENV="") → deploy succeeded 🟢. Via SOCKS proxy: HTTP 200; served cert subject: CN=*.ci.commoninternet.net, SAN-matched, SSL certificate verify ok, issuer LE E8 — i.e. the pre-issued wildcard, NOT a per-host ACME cert.
  • (c) No Gandi/DNS token, no ACME credential: repo (all history) clean; on host the only gandi/dns-challenge strings are commented-out recipe-template options (#GANDI_…, #SECRET_GANDIV5_…) holding no value. Active traefik env = LETS_ENCRYPT_ENV= (empty), WILDCARDS_ENABLED=1, compose.wildcard.yml. staging/production certResolvers are defined in traefik.yml (stock template) but referenced by no router; both acme.json are 0 bytes; 0 ACME lines in traefik logs. No ACME ever fires. (Hardening risk filed — see findings.)
  • (d) Manual renewal documented: DECISIONS.md — operator re-issues at same paths, then abra app secret rm … ssl_cert + re-insert at bumped version; install.md "Renewed out-of-band; never ACME here."
  • Teardown: abra app undeploy + volume remove → post-teardown services/containers/volumes/ secrets for the probe all 0. Also independently confirmed the Builder's cchtml1 test left 0 runtime resources (only its inert .env config file remains, harmless).

Verdict: M1 PASS. Not a hard fail on (c) — no token/credential exists and no ACME fires — but the inert ACME resolvers + test-app default LETS_ENCRYPT_ENV=production are a latent hazard that goes live when the harness deploys apps; filed as [adversary] for M4.

M2 — Drone online: PASS @2026-05-26T23:32Z

Verified cold from own clone. Acceptance: "push to cc-ci triggers a visible green Drone build."

  • Drone server healthy: https://drone.ci.commoninternet.net/healthz → HTTP 200 via gateway. Exec runner (drone-runner-exec.service) active, polling the remote server capacity=2 type=exec.
  • Repo wired: in Drone's DB the recipe-maintainers/cc-ci repo is repo_active=1, repo_config=.drone.yml. Gitea↔Drone OAuth proven by the in-pipeline clone step succeeding against the private repo (build can't clone without working OAuth/repo token).
  • Push→green, independently triggered: I pushed my own commit 91a8e8d (a REVIEW.md change) → Drone created build #4, build_event=push, build_trigger=@hook (Gitea webhook), and it ran success: stage self-test exit 0, steps clone+hello both exit 0. Builds #1#3 (Builder commits) likewise all success via @hook. (My earlier M0/M1 review pushes predate the .drone.yml, so correctly produced no builds.)
  • Visible logs (D7 precondition): logs table holds per-step log blobs for every build; Drone UI/API serve them. Full D7 UX is M8.

Verdict: M2 PASS. No new findings.

M3 — Comment bridge: PRE-CLAIM PROGRESS (not yet PASS) @2026-05-26T23:48Z

M3 is Blocked in STATUS (Gitea not delivering webhooks), so not a gate verdict yet. But the bridge is deployed and I independently hammered its auth/filter logic — the part I can verify regardless of the delivery leg (and which survives a pivot to API polling). Probes were live POSTs to https://ci.commoninternet.net/hook via the SOCKS proxy, with HMAC signatures I computed from the on-host secret (read with root; value never printed/committed):

probe expect got
no X-Gitea-Signature 401 401
bad signature 401 401
valid sig, event=ping (not issue_comment) 204 204
valid sig, !testmexyz on a real PR 204 (no trigger) 204
valid sig, !testme but issue is not a PR 204 204
valid sig, !testme on PR, action=edited 204 204
valid sig, !testme on real PR, non-collaborator 403 403

So: HMAC fail-closed + timing-safe (compare_digest, verified before body parse), !testmexyz correctly ignored (exact trimmed match), non-PR ignored, and a non-collaborator is rejected (403; collaborator status re-checked via Gitea API, not trusted from the signed payload). Source review of bridge/bridge.py found no auth bypass.

Blocker independently corroborated (operator-side): the bridge hook is registered + active on recipe-maintainers/cc-ci (id 210, events [issue_comment]ci.commoninternet.net/hook), and the bot is not a Gitea site-admin (GET /admin/hooks → 403) nor org owner, so it genuinely cannot inspect/change Gitea's [webhook] ALLOWED_HOST_LIST. Endorse STATUS ## Blocked: needs operator allowlisting or the documented poll-the-API fallback.

Still UNVERIFIED for an M3 PASS: (1) the positive path — a valid collaborator !testme actually starts a build + posts the PR comment end-to-end; (2) real Gitea→bridge delivery (or the polling pivot). Will complete both when M3 is claimed.

Noted for M7 (not a finding yet): the Drone-managed Gitea webhook (id 209) carries its webhook secret as a ?secret= query param in the hook URL (Drone default; admin-only in Gitea, not in cc-ci git / CI logs / dashboard). Will adjudicate against D6 at M7.

M4 — Harness + install stage: VERIFICATION IN PROGRESS (no verdict yet) @2026-05-27T00:35Z

M4 is CLAIMED. Code review done; runtime checks so far:

  • A1 CLOSED (see BACKLOG): harness forces LETS_ENCRYPT_ENV="" every deploy; live app cust-c95a69 served the wildcard cert, 0 ACME lines, no certresolver.
  • Happy-path teardown works: a prior run's app cust-e084bd was fully torn down (gone) — not an orphan; earlier ambiguity was a run cycling apps.
  • Two teardown-robustness defects filed (A2, A3): janitor's -pr filter is dead code under the cust-<hex> naming (no crash-orphan reaping); teardown is best-effort/unverified and deletes the .env even on failed undeploy (silent orphan, run still green).
  • Deferred to next idle tick (a Builder harness run is active now; sequential-only): my own cold install run (green install + Playwright + clean teardown verification) and the §6 kill-mid-run probe to test A3 empirically. Verdict (PASS/FAIL) follows that.

M4 — Harness + install stage: PASS @2026-05-27T01:05Z

Verified by my own cold harness run (RECIPE=custom-html REF=advcold… cc-ci-run runner/run_recipe_ci.py, app cust-cfeb6a, isolated from a Builder run that happened to run concurrently as cust-3c1970 — no collision, distinct domains/volumes/secrets):

  • Install stage green: test_install.py → 2 passed (27s): test_http_reachable (HTTPS 200 via gateway) + test_playwright_page (real Chromium loads the live app, status 200, served HTML).
  • Guaranteed teardown: after the run, cust-cfeb6a left 0 services / volumes / secrets / containers / .env — fully clean. Infra (traefik/drone/bridge/backups) untouched.
  • A1 closed (no-ACME enforced). Open robustness findings A2 (dead -pr janitor) + A3 (unverified best-effort teardown) concern the crash path (finalizer-skipped), not this happy-path run; they don't block M4's literal acceptance but must be resolved before DONE (D2 teardown guarantee). Kill-mid-run probe to substantiate A2/A3 deferred until the host is idle.

Verdict: M4 PASS.

M5 — Upgrade + backup/restore stages: PASS @2026-05-27T01:05Z

Same cold run, stages 2 and 3 — both genuine end-to-end (no mocks; assertions reviewed in source and not softened):

  • Upgrade green: test_upgrade.py → 1 passed (41s). Deploys the previous published version (previous_version = recipe_versions[-2]), writes a marker into the volume-backed html dir, upgrades to latest (abra upgrade), then asserts HTTP 200 and the marker survives — a real version change with data persistence across the volume (cust-…_content), not a no-op.
  • Backup/restore green: test_backup.py → 1 passed (37s). Writes original, abra backup, mutates to mutated (asserted), abra restore, then asserts the served content is back to original ("restore did not return the pre-mutation state"). Real backup→mutate→restore cycle via backup-bot-two.
  • Teardown clean (same cust-cfeb6a 0-remnant check above covers all three stages — same domain reused per stage).

Verdict: M5 PASS.

M6 — Recipe-local tests + second recipe: VERIFICATION IN PROGRESS (no verdict yet) @2026-05-27T01:48Z

M6 CLAIMED. Host has been continuously busy (Builder M6.5 ramp), so deploy-based checks are deferred to an idle window; static + evidence review so far:

  • custom-html 3-stage: already verified cold by me (see M5 PASS) — green + clean teardown.
  • D4 recipe-local discovery — code genuine: run_recipe_ci.snapshot_recipe_tests copies the recipe-shipped tests/ before abra re-checkouts to a version tag, then run_recipe_local deploys the app and runs those tests against the LIVE app via CCCI_BASE_URL/CCCI_APP_DOMAIN, merged as a separate stage with guaranteed teardown. Demo branch recipe-maintainers/custom-html@ ci/d4-recipe-local confirmed to ship tests/test_recipe_local.py (Gitea API). Will run it cold to confirm the stage executes+passes.
  • keycloak (#2) install — test genuine: /realms/master 200 health + real Playwright admin console login (waits for the username field). recipe_meta.py (HEALTH_PATH/timeouts) confirms D5 "no harness surgery". Empirical keycloak reproduction deferred (heavy deploy; idle window).
  • Filed [adversary] A4 (concurrency): same-recipe concurrent runs share ~/.abra/recipes/<recipe> with no isolation/lock/concurrency-cap — a collision vector for the §6 concurrency check; to confirm empirically.

Pending for idle host: cold D4 run, keycloak reproduce, A2/A3 kill-probe re-test, A4 concurrency test.

D6/M7 — preliminary leak scan of published Drone logs (PASS so far; M7 not yet claimed) @2026-05-27T02:05Z

Host-safe probe while the host was busy. Pulled Drone's database.sqlite, dumped all 42 logs rows (~25.5k chars of published per-step build output), scanned:

  • Known infra secrets — 0 leaks: webhook HMAC (64), drone token (32), gitea token (40) each appear 0× in the logs (exact grep -F).
  • No value patterns: 0 matches for password|secret|token = <value>.
  • The only long hex/base64 hits are git commit SHAs in git clone/merge output — benign. Caveat: current Drone logs are hello-world + self-test; the full M7/D6 test must also cover app-generated secrets (e.g. keycloak DB passwords) in recipe-run logs AND the dashboard (M8). This is a clean baseline, not the final D6 verdict. (DB copy was scanned off-box and deleted; no secret value printed or committed.)

M3 — Comment bridge: PASS @2026-05-27T03:13Z

Verified cold against the NEW design (orchestrator change: polling-PRIMARY + org-membership auth; webhook now optional). Re-reviewed bridge/bridge.py (256 lines) — sound — then live-probed the running bridge + Drone:

  • !testme triggers a run ≤60s: I posted !testme (comment 13708) on PR #1 at epoch 1779847690 → bridge [poll] triggered build 35 → Drone build 35 created at 1779847702 = 12s latency. (Build is failure only because RECIPE=cc-ci has no tests/cc-ci/; the trigger + event=custom recipe-CI pipeline fired correctly — integration is live.)
  • Re-commenting re-runs: my new comment 13708 → build 35, distinct from the earlier comment 13705 → build 26. Distinct comment ids each fire once (dedup via _claim).
  • Other comments do NOT trigger: I posted !testmexyzno build created, no bridge trigger log. Exact trimmed match enforced.
  • Auth enforced (org-membership, fail-closed): GET /orgs/recipe-maintainers/members/<u> — autonomic-bot & notplants → 204 (allowed), definitely-not-a-member-zzz9 → 404 (rejected). is_authorized returns True only on 204/allowlist; anything else (incl. errors) → False.
  • Link back: bridge posted run-link comment 13706 ("cc-ci: started CI run … → drone…/recip…").
  • Concurrency cap live: runner capacity=1 (DRONE_RUNNER_CAPACITY=1) + pipeline concurrency:limit:1 — recipe-CI builds serialize.

Verdict: M3 PASS. (Polling is outbound read+comment only — no repo-admin; webhook optional.) Note: full bridge→3-stage-recipe-CI E2E on a real recipe PR is the Builder's in-flight integration item / D10 — build 35 shows the pipeline wiring works; green-on-a-real-recipe is M10.

D6 — leak scan extended to recipe-CI build logs (still clean) @2026-05-27T04:05Z

Followup to the earlier hello-world scan: scanned the logs of all 7 event=custom recipe-CI builds (~26.7k chars — these ran real abra app deploy + abra app secret generate, so generated app secrets could surface here). Result: 0 password|secret = <value> patterns, 0 "secret generated/inserted" value lines (abra doesn't echo secret values), and every long hex/base64 hit is benign — Nix store paths, git SHAs, Drone workspace dir names (<rand16>/drone/src), pytest tracebacks. No app-secret leak in published recipe-run logs. (Full M7/D6 verdict still pending the dashboard (M8) leak check + final M7 claim.)

M6 — Recipe-local tests + second recipe: PASS @2026-05-27T04:43Z

Acceptance: "both recipes green (custom-html 3-stage; keycloak install) + recipe-local merged", plus D4/D5. Verified by a mix of my own cold runs + deep Drone-log corroboration (keycloak's 31-min deploy made a self-rerun impractical on the contended host, so I read the actual build #39 logs, not a Builder summary):

  • custom-html 3-stage: my own cold run (see M5 PASS) — install/upgrade/backup green, 0 orphans.
  • keycloak (#2) full 3-stage — build #39 (event=custom, RECIPE=keycloak, success): actual log lines show PASSED test_realm_endpoint_healthy, PASSED test_playwright_admin_login (install, 510s), PASSED test_upgrade_preserves_realm (upgrade, 610s — DB realm survived), PASSED test_backup_mutate_restore (backup, 495s — realm restored). Three separate reported stages (D2). Tests are genuine (admin REST + real Playwright admin-console login; reviewed source — not mocked). Post-run: 0 keycloak services/volumes (clean teardown).
  • D4 recipe-local — verified by my OWN run: RECIPE=custom-html SRC=…/custom-html REF=ci/d4-recipe-local → recipe-shipped tests/test_recipe_local.py snapshotted to a temp dir (immune to abra's version re-checkout), deployed the app, ran test_recipe_local_serves_content PASSED against the LIVE app via CCCI_BASE_URL, merged as a recipe-local stage; clean teardown (0 cust- leftovers).
  • D5 (no harness surgery): keycloak enrolled via tests/keycloak/ + recipe_meta.py only; no changes to shared runner/harness code. enroll-recipe.md documents the flow.

Verdict: M6 PASS. (keycloak full 3-stage also satisfies the first M6.5 breadth slot.)

M6.5 — breadth ramp: RUNNING EVIDENCE (no verdict yet — recipes 56 + gate pending) @2026-05-27T06:12Z

Deep-corroborating each recipe's canonical Drone recipe-ci build from its actual logs (genuine 3-stage assertions, not summaries). Confirmed green so far (categories in parens):

  • custom-html (simple/stateless) — build #33 + my own cold 3-stage run (M4/M5).
  • keycloak (SSO + DB-backed) — build #39: realm health + Playwright admin login (install), test_upgrade_preserves_realm, test_backup_mutate_restore (M6 verdict).
  • cryptpad (stateful, no external DB) — build #46: test_http_reachable, test_playwright_loads_cryptpad, test_upgrade_preserves_data, test_backup_mutate_restore.
  • matrix-synapse (large-volume / DB + media store) — build #51: test_client_api_healthy, test_client_api_advertises_versions, test_upgrade_preserves_data, test_backup_mutate_restore. All three stages reported separately per build (D2). Categories covered: simple, SSO/DB, stateful, large-volume. Remaining: recipe #5/#6 (multi-service+S3/object-storage, e.g. lasuite; and the 6th for breadth) + the M6.5 gate. Final M6.5/D10 verdict after those + the §6 concurrency check.

Reconciliation @2026-05-27T06:18Z (watchdog ping)

Checked all standing claims: every CLAIMED milestone gate through M6 is Adversary-PASS — M0 @21:35, M1 @22:20, M2 @23:32, M3 @03:13, M4 @01:05, M5 @01:05, M6 @04:43 (all <24h). The "Gate: M0/M1/M2/M3 — CLAIMED, awaiting Adversary" strings still present in STATUS.md §Gates are stale (already cleared here); a watchdog scanning that section may false-positive on them — Builder may want to annotate them PASS. No open milestone claim right now: M6.5 is in-flight (4/6 recipes corroborated green: custom-html/keycloak/cryptpad/matrix-synapse; recipes 56 + the M6.5 gate pending), M7/M8/M9/M10 not yet claimed. Open findings: A2 (live janitor sweep pending an idle host; mechanism already verified). Nothing for me to verify is currently blocked on me.

M6.5 — Breadth ramp (recipes 36): PASS @2026-05-27T07:25Z

Acceptance: "recipes 36 each full three-stage green; enrolling N≥3 needed no shared-harness changes." All six recipes' canonical Drone recipe-ci builds deep-corroborated from their actual logs (genuine assertions + 3 separately-reported stages each; clean teardown):

  • cryptpad #46 (stateful) — http + Playwright, test_upgrade_preserves_data, test_backup_mutate_restore.
  • matrix-synapse #51 (large-volume/DB+media) — test_client_api_healthy/_advertises_versions, test_upgrade_preserves_data, test_backup_mutate_restore.
  • lasuite-docs #57 (multi-service + S3/MinIO) — test_http_reachable, test_playwright_loads_frontend, test_upgrade_preserves_data, test_backup_mutate_restore.
  • n8n #63 (workflow) — test_healthz, test_playwright_loads_editor, test_upgrade_preserves_data, test_backup_mutate_restore. (recipes 12 custom-html #33/keycloak #39 verified under M4/M5/M6.)
  • D5 (no harness surgery) verified: grepped shared harness (runner/harness, conftest, run_recipe_ci) — no per-recipe branching (if recipe==…); the only recipe names there are comments. Per-recipe quirks (cryptpad SANDBOX_DOMAIN, health paths, timeouts) live in tests/<recipe>/recipe_meta.py and are consumed via the generic EXTRA_ENV/meta hook in deploy_app. Enrolling a recipe = tests/<recipe>/ + recipe_meta.py only.
  • bluesky→n8n swap is plan-sanctioned + documented (DECISIONS): bluesky-pds needs TLS-passthrough to an in-container caddy doing its own ACME — incompatible with the no-DNS-token/no-ACME design; documented non-CI'd recipe (per §2's explicit allowance). The 5 required D10 categories (simple/SSO+DB/stateful/large-volume/multi-service+S3) are covered without it.

Verdict: M6.5 PASS. Note: these builds were triggered as recipe-ci custom builds (RECIPE param); the real !testme-on-a-PR end-to-end for the breadth set is D10/M10, still to verify.

M7 — Secrets hardening (D6): PASS @2026-05-27T07:55Z

Acceptance: "Adversary's secret-grep over published logs finds nothing; rotation doc followed." Verified the §9 hard rule (no plaintext secret in git, logs, or UI) across ALL surfaces:

  • Published Drone logs — clean: dumped every logs row across all builds (~119k chars; incl. the 6 recipe runs that generate app secrets). The 3 infra secrets (webhook HMAC / drone token / gitea token, read from /run/secrets) each appear 0×; no password|secret|token=<value> patterns; long-token hits are git SHAs / nix paths / Drone workspace names (benign).
  • Dashboard — clean: https://ci.commoninternet.net/ (200) + /badge/*.svg: 0 secret patterns, 0 infra-secret values.
  • Git (all history) — clean: each infra secret 0×; secrets/secrets.yaml is sops-encrypted (7× ENC[…]). No plaintext infra secret committed.
  • Redaction filter (run_recipe_ci.run_stage_redacted): masks any /run/secrets/* value (≥8 chars) in stage stdout before it reaches Drone. Present as a safety net; 0 REDACTED markers in logs = no secret was ever echoed in the first place.
  • Rotation doc (docs/secrets.md) matches reality: .sops.yaml has exactly the documented two recipients — host key age1h90ut… (from cc-ci's ed25519 SSH host key) + off-box master recovery age1cmk26t…; sops-nix decrypts to /run/secrets/<name> (0400 root) using the SSH host key (verified at M0 + present now). A1/A2 split + rotation steps are coherent.

Minor (not a finding): the redaction list covers infra secrets only, not per-run generated app secrets — but abra doesn't echo generated secrets (recipe logs clean) so no app-secret ever surfaced.

Verdict: M7 PASS.

M8 — Dashboard (D7): PASS @2026-05-27T08:10Z

Acceptance: "overview matches reality across several runs; outcomes mirrored to PR comments."

  • Overview matches reality: https://ci.commoninternet.net/ lists all 6 enrolled recipes, each success with the exact canonical build #s I independently corroborated (cryptpad #46, custom-html #33, keycloak #39, lasuite-docs #57, matrix-synapse #51, n8n #63) + relative "last run" times; cc-ci itself correctly excluded; 30s auto-refresh; YunoHost-CI-like recipe table + status badges, dark theme.
  • Status badges: /badge/keycloak.svg encodes success (per-recipe embeddable badge).
  • PR-comment outcome reflection: on PR #1 the bridge posted a start comment (id 13709 → run #35) and a final-outcome comment (id 13712: "run for cc-ci @ d397720a failure → …/76") — mirrors the final pass/fail and links the run. (Failure case shown; success path is the same code.)
  • No secret leak on the dashboard/badges (verified under M7).

Verdict: M8 PASS. (A green outcome reflected on a real recipe PR is exercised at D10/M10.)

M10/D10 — independent confirmation of the Docker Hub rate-limit blocker @2026-05-27T10:25Z

The Builder filed lasuite-docs upgrade failing on Docker Hub anonymous pull rate limits (A1 registry creds needed; 5/6 recipes green via real !testme). I disbelieved and verified — it is real, not a masked harness defect:

  • Queried Docker Hub's rate-limit headers from cc-ci's own source IP (68.14.43.142): ratelimit-limit: 100;w=21600, ratelimit-remaining: 1 — i.e. ~1 anonymous pull left in the 6h window. The D10 breadth runs (6 recipes, lasuite alone = 9 images) drained the anonymous quota.
  • lasuite Drone builds (#88/#92 failure, #93 killed) show no toomanyrequests in pytest output — expected, because a rate-limited pull manifests at the docker/swarm task layer (deploy/health timeout), not in the test log; the header check is the direct proof.
  • The CI system itself is sound: lasuite install + backup are green; only the upgrade stage (most image pulls) is gated, and only by the external quota. This is precisely the plan's anticipated A1 input (§1.5/§4.4: "rate-limit failure traced to this is a finding, then request creds").

Consequence for DONE: D10 requires all 6 recipes green via real !testme with all 3 stages. lasuite-docs upgrade cannot reliably pass without authenticated registry pulls. This is an operator-action blocker (provide Docker Hub creds → sops secrets/), analogous to the M3 webhook whitelist. Not a VETO of system quality; a missing external input. DONE must wait until lasuite's upgrade goes green via !testme (creds provided, or quota-window retry verified stable).

M10/D10 — real-!testme proof: 5/6 VERIFIED (6th blocked on registry creds) @2026-05-27T10:42Z

Independently verified the full real-!testme path (D1 trigger + D2 three genuine stages + D7 outcome reflection) for 5 of 6 recipes, from a cold read of Drone + bridge logs + Gitea PR comments:

recipe build bridge poll-trigger (real !testme) stages result
custom-html #84 PR#2 comment 13717 3 (4 asserts) success
keycloak #86 PR#1 comment 13719 3 (4 asserts) success
matrix-synapse #87 PR#1 comment 13720 3 (4 asserts) success
n8n #89 PR#1 comment 13722 3 (4 asserts) success
cryptpad #90 PR#2 comment 13727 3 (4 asserts) success
  • Each build is event=custom with REF=PR-head sha (tests the PR's code, D1), 3 separately-reported stages install/upgrade/backup (D2), and the bridge logged a genuine [poll] triggered build N … by autonomic-bot for each (real comment, not a manual build).
  • Outcome reflection (D7): verified on keycloak PR#1 — !testme → bridge comment "run for keycloak @ 04400dff passed → …" (success path; failure path seen earlier on cc-ci).
  • 6th recipe lasuite-docs: install+backup green via !testme, upgrade blocked on the Docker Hub anon rate limit (independently confirmed: remaining 1/100). Category = multi-service + S3/object-storage; until its upgrade is green via !testme, D10 is not fully met (5/6).

Verdict: D10 PARTIAL (5/6) — pass for 5; the 6th awaits operator registry creds. No system defect; the gap is the external pull quota. DONE must wait for lasuite's 3rd stage green via !testme.