Compare commits

...

26 Commits

Author SHA1 Message Date
369f4f486b review(bsky): M1 PASS @73889ed — root cause reproduced cold (:0.4=0.5.1/index.ts crash, :0.4.219=index.js fix); PR#2 minimal +2/-2 unmerged; run 427 genuine drone !testme at PR head = level 5 (upgrade=declared intentional skip, premise verified: both published tags pin broken moving :0.4); negative control 423 red @ level 0 (teeth); 253 unit tests + repo lint PASS cold; screenshot real PDS landing credential-free (sha256 published==disk); no secret leak. No gate weakening — EXPECTED_NA scoped per-recipe-per-rung. No VETO.
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 12:03:04 +00:00
cba53b69a4 status(bsky): operator summary written (B9); journal: shot-phase N/A disposition superseded, no canonical to reseed (B8 complete)
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:58:34 +00:00
f1500123e7 docs(deferred): bluesky-pds entry RESOLVED — fix PR#2 open (re-pin 0.4.219), green run 427 level 5 at PR head, screenshot real; pointers to upstream registry + decisions
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:57:12 +00:00
cfda9e72db review(bsky): EXPECTED_NA['upgrade'] premise verified cold — both published tags (0.1.1/0.2.0+v0.4) pin broken moving :0.4, no deployable base; recorded scoping/teeth checks for the claim
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:56:07 +00:00
73889ed860 claim(bsky): M1 — root cause proven (:0.4 republished w/ 0.5.1/index.ts vs entrypoint index.js), mirror PR#2 re-pin 0.4.219 green at head via drone run 427 (level 5, upgrade=declared intentional skip, negative control run 423), screenshot verified real+credential-free
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:55:41 +00:00
72b3d6c089 journal(bsky): run 423 red = upgrade-base trap (base 0.1.1+v0.4 pins broken :0.4, PR head never reached); decisions entry for EXPECTED_NA-upgrade base suppression; run 427 in flight
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:52:39 +00:00
e9745c8c74 feat(bsky): EXPECTED_NA['upgrade'] suppresses the upgrade-tier base deploy — single deploy = PR head; bluesky-pds declares it (no deployable base: every published tag pins the republished moving :0.4). upgrade_base() extracted pure + 6 unit tests; meta-key doc regenerated. 253 unit tests + repo lint PASS
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2026-06-11 11:51:12 +00:00
f88c6bc78d review(bsky): cold image probe reproduces root cause both halves (:0.4 ships index.ts/node24, :0.4.219 ships index.js/node20); recorded M1 scrutiny points; no claim yet
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:44:26 +00:00
823023a19a docs(deferred): operator housekeeping pass 2026-06-11
All checks were successful
continuous-integration/drone/push Build is passing
- CLOSED: plausible enrollment (overtaken — enrolled+running), discourse
  bitnami pin (superseded — enrolled, L4 baseline), immich pg_dump (PR#2
  green, operator merge pending), plausible Q4.7b ClickHouse (PR#3 green,
  operator merge pending)
- RE-ENTERED per operator: mailu backupbot -> phase mailu, drone enrollment
  -> phase drone, uptime-kuma create-a-monitor -> phase kuma, discourse
  abra-stamp drift -> phase dstamp, bluesky-pds -> phase bsky (in progress)
2026-06-11 11:42:12 +00:00
fc16250db2 status(bsky): bootstrap phase — root cause proven (:0.4 moving tag now ships 0.5.1/node24/index.ts; recipe entrypoint execs index.js), fix = exact-pin 0.4.219; decisions + upstream registry
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2026-06-11 11:37:28 +00:00
8d5bf305e8 review(bsky): seed REVIEW-bsky + cold baseline recon (image :0.4 moving tag, entrypoint runs relative index.js); awaiting first claim
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:32:20 +00:00
9ce987188a status(lvl5): ## DONE — M1 (cfc87fd) + M2 (13cad1f) both PASS, no VETO; L5 lint rung + de-capped levels live end-to-end; cleanup complete
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:29:32 +00:00
13cad1f985 review(lvl5): M2 PASS @a521d43 — proven in real CI from cold clone of main. 247 unit tests + PR-path regression green, repo lint PASS. Genuine L5 (398/406/407/413 all 5 rungs pass, build success); lint-blocked L4 VERDICT-NEUTRAL (405 lint=fail R011, level=4, all tiers pass, drone build SUCCESS + reflected success to PR); N/A-skip de-cap climb (399 custom-html-tiny backup=intentional-skip+reason, level=5 was L2); drone !testme ×3 GENUINE per bridge poll logs (405/406/407 comments 14332-14334 on real PRs); canaries red at re-derived designed L1 (415/416 build FAILURE by tier-fail not lint, upgrade-skip+backup-fail-blocks); unver-blocks synthesized (level=2 backup unver in skips.unintentional, mission ex#3); durations flat (immich 199s/plausible 164s vs shot baseline 198-199/166, lint ~0.7s); old schema-1 artifacts render 200 no relabel; lint.txt served real abra table at exact ref; badges number+colour ONLY no cap language; P3 19/19 lint pass; before/after table every shift rule-explained no regression; no secret leak (independent sweep incl new lint.txt surface). §6 DoD satisfied. No VETO — Builder cleared to write ## DONE.
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:28:19 +00:00
a521d43a17 claim(lvl5): M2 — P4 proven in real CI: L5 (398/406/407/413), lint-blocked L4 verdict-neutral (405), N/A-skip climb (399), drone !testme ×3, canaries red @ re-derived L1 (415/416), unver-blocks synthesized run L2, old artifacts render, durations at baseline, visuals verified
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:18:26 +00:00
dc924c679b status(lvl5): before/after table real values (398/399/405/406/407/413) + canary designed-level re-derivation (415/416 red @ L1)
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 11:15:31 +00:00
763f8d1a47 journal(lvl5): P4 wave 2 — PR-path lint fix proven, L4-blocked + 2×L5 PR proofs green, visuals verified
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2026-06-11 11:04:21 +00:00
68c3486216 fix(lvl5): lint executor PR-path — abra lint selects+checks out the repo DEFAULT BRANCH; scratch clone of a detached per-run tree has none (FATA, live 400-402), and a stale default would be silently linted instead of the PR head. Force local main AT the tested ref + repoint origin to the scratch itself (offline tag fetch, no drift). Regression test with detached two-commit source proves exact-ref content is linted. 247 unit tests green; real-abra detached-source smoke pass.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2026-06-11 10:56:56 +00:00
1fb70aafa6 journal(lvl5): P4 wave 1 — hedgedoc L5 + custom-html-tiny N/A-skip climb green; lint-demo PR4 + 3 testme builds in flight
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 10:50:00 +00:00
29047a8dec status(lvl5): M1 PASS consumed — merged 08e6cc8, suite green on merged main, dashboard rolled + live-verified; starting P4
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2026-06-11 10:46:03 +00:00
08e6cc8273 feat(lvl5): merge phase-lvl5 → main after M1 PASS (review cfc87fd) — implementation content taken verbatim from the Adversary-verified branch tip 3d8d286
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 07:56:34 +00:00
cfc87fd8d3 review(lvl5): M1 PASS @3d8d286 — cold clone HEAD-match, 246 unit tests green + repo lint PASS on CI venv; de-capped compute_level correct on all 4 mission worked examples (L1 fail-blocks, L5 skip-climbs, L2 unver-blocks, L4 lint-unver); derive_rungs N/A classification matches DECISIONS table incl subtle upgrade structural-skip vs abort-unver split; §2.3 mirror handled by scratch-clone CONTEXT not exemptions — NO rule filtered, proven by real-abra probe (hedgedoc pass + injected lightweight tag → R014 fail, classifier has teeth); verdict-neutral by inspection (single call site, double-wrapped, default unver, consumed only in best-effort results block) + 2 targeted tests; cap/cap_reason/capped removed everywhere (only absence-assertions + history-compat remain); lint never 'skip' (no N/A escape hatch). No VETO — Builder cleared to merge + proceed to M2.
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 07:55:35 +00:00
5ce813e910 journal(lvl5): P3 sweep evidence
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 07:54:50 +00:00
40caaab8fb status(lvl5): P3 sweep complete — 19/19 enrolled recipes lint PASS (warn-only misses), no mirror PRs needed; before/after baseline table assembled
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 07:54:35 +00:00
24baac559c claim(lvl5): M1 — P1+P2 complete on phase-lvl5 @ 3d8d286; 246 unit tests cold-green on cc-ci venv, repo lint PASS, real-abra smoke pass+R014-fail, verdict-neutral by construction; main holds reverts pending pre-merge PASS
All checks were successful
continuous-integration/drone/push Build is passing
2026-06-11 07:51:13 +00:00
cd62743055 Revert "feat(lvl5): P1 — 5-rung ladder (L5=abra recipe lint) + de-capped level semantics"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit e219a7891d.
2026-06-11 07:46:57 +00:00
589943f46e Revert "docs(lvl5): results-ux.md → 5-rung de-capped ladder + schema 2; recipe-customization.md EXPECTED_NA/BACKUP_CAPABLE rows to new semantics"
This reverts commit af7488a498.
2026-06-11 07:46:57 +00:00
17 changed files with 1072 additions and 50 deletions

18
BACKLOG-bsky.md Normal file
View File

@ -0,0 +1,18 @@
# BACKLOG — phase bsky
## Build backlog
- [x] B1: Root-cause diagnosis — inspect recipe compose/entrypoint + actual `:0.4` image vs exact tags on cc-ci (2026-06-11)
- [x] B2: Upstream research persisted to cc-ci-plan/upstream/bluesky-pds.md (plan repo f395247)
- [x] B3: DECISIONS.md entry — pin choice (exact 0.4.219 over 0.5.1-main / digest pin), version label bump
- [x] B4: Mirror PR branch `upgrade-0.3.0+v0.4.219` — compose.yml re-pin + label bump; open PR on recipe-maintainers/bluesky-pds
- [x] B5: `!testme` on the PR → full lifecycle green (install/health, upgrade-path status justified, backup/restore, functional, L5 lint); record level under de-capped semantics + reconcile expected baseline
- [x] B6: Screenshot on the green PR run — verify PNG real/representative/credential-free (Read it); SCREENSHOT hook only if needed
- [x] B7: Claim M1 (root cause + green fix PR + screenshot verified)
- [ ] B8: Close DEFERRED bluesky entries with pointers; JOURNAL note updating shot-phase N/A disposition
- [ ] B9: Operator handoff summary in STATUS-bsky.md (what was wrong, what the PR changes, post-merge expectations incl. canonical/warm reseed)
- [ ] B10: Claim M2
## Adversary findings
(Adversary-owned)

View File

@ -2,17 +2,98 @@
## Build backlog
- [ ] B1 (P1) `level.py`: append rung `lint` (L5); new status vocabulary {pass, fail, skip, unver}; `compute_level()` → new formula (level = max i: rung_i pass ∧ ∀j<i status {pass,skip}); DELETE cap_reason/capped concepts.
- [ ] B2 (P1) lint executor (`harness/lint.py`): `abra recipe lint <recipe>` against the exact tested ref; hard ~60s timeout; rc+full output `lint.txt` artifact; pass/fail/unver classification (missing abra / timeout / exception unver, never pass, never skip); mirror-context handling per phase-plan §2.3 (probe abra behavior first; any filtering = named + unit-tested + DECISIONS.md).
- [ ] B3 (P1) `results.py`: wire lint into `derive_rungs` + explicit intentional-vs-unintentional classification of EVERY N/A source; drop level_cap_reason/level_cap_rung from schema; `skips()` reflects new statuses; orchestrator (`run_recipe_ci.py`) runs lint executor at the tested-ref point + passes result through; verdict-neutral (R7 wrap).
- [ ] B4 (P1) unit tests: rewrite test_level.py/test_results.py to new semantics incl. mission worked examples (fail-blocks L1; intentional-skip climbs L5; unver-blocks L2; lint unver L4; unclassifiable N/A unver default); lint executor tests; old-artifact rendering compat tests.
- [ ] B5 (P2) `card.py`: 05 color ramp; cap line removed ("level N of 5" neutral); rung table renders ✔/✘/intentional-skip/unverified; level_badge_svg loses cap_skip third segment (badge = number+color only); tolerate old artifacts.
- [ ] B6 (P2) `dashboard.py`: _LEVEL_COLOR 5-scale; _level_pill/badge SVG number-only; legend text; old results.json (cap_reason present, lint absent) render without KeyError.
- [ ] B7 (P2) docs: results-ux.md, testing.md, recipe-customization.md §EXPECTED_NA wording L5 ladder, de-cap semantics.
- [ ] B8 (P1) DECISIONS.md: semantics change record (replaces Phase-3 "N/A caps"); N/A classification table (every derive_rungs N/A source intentional|unintentional); mirror-filter decision for lint (if any filtering).
- [ ] B9 gate M1: claim (branch w/ P1+P2; clean tree; cold-verifiable).
- [ ] B10 (P3) lint sweep over ALL enrolled recipes (scratch clones never touch ~/.abra/recipes during builds); matrix here (pass/fail + rule hits); mechanical fixes mirror PRs (never push main/never merge); rest DEFERRED.md.
- [ ] B11 (P4) real-CI proofs: 1 genuine L5; 1 lint-blocked L4 (synth branch ok); 1 N/A-skip climb; 2× drone !testme; canary suite at re-derived designed levels; 1 synthesized unver-blocks run; before/after level table for ALL enrolled recipes; card/dashboard PNG/SVG visually verified.
- [ ] B12 gate M2: claim; then ## DONE after fresh PASS.
- [x] B1 (P1) `level.py`: append rung `lint` (L5); new status vocabulary {pass, fail, skip, unver}; `compute_level()` → new formula (level = max i: rung_i pass ∧ ∀j<i status {pass,skip}); DELETE cap_reason/capped concepts.
- [x] B2 (P1) lint executor (`harness/lint.py`): `abra recipe lint <recipe>` against the exact tested ref; hard ~60s timeout; rc+full output `lint.txt` artifact; pass/fail/unver classification (missing abra / timeout / exception unver, never pass, never skip); mirror-context handling per phase-plan §2.3 (probe abra behavior first; any filtering = named + unit-tested + DECISIONS.md).
- [x] B3 (P1) `results.py`: wire lint into `derive_rungs` + explicit intentional-vs-unintentional classification of EVERY N/A source; drop level_cap_reason/level_cap_rung from schema; `skips()` reflects new statuses; orchestrator (`run_recipe_ci.py`) runs lint executor at the tested-ref point + passes result through; verdict-neutral (R7 wrap).
- [x] B4 (P1) unit tests: rewrite test_level.py/test_results.py to new semantics incl. mission worked examples (fail-blocks L1; intentional-skip climbs L5; unver-blocks L2; lint unver L4; unclassifiable N/A unver default); lint executor tests; old-artifact rendering compat tests.
- [x] B5 (P2) `card.py`: 05 color ramp; cap line removed ("level N of 5" neutral); rung table renders ✔/✘/intentional-skip/unverified; level_badge_svg loses cap_skip third segment (badge = number+color only); tolerate old artifacts.
- [x] B6 (P2) `dashboard.py`: _LEVEL_COLOR 5-scale; _level_pill/badge SVG number-only; legend text; old results.json (cap_reason present, lint absent) render without KeyError.
- [x] B7 (P2) docs: results-ux.md, testing.md, recipe-customization.md §EXPECTED_NA wording L5 ladder, de-cap semantics.
- [x] B8 (P1) DECISIONS.md: semantics change record (replaces Phase-3 "N/A caps"); N/A classification table (every derive_rungs N/A source intentional|unintentional); mirror-filter decision for lint (if any filtering).
- [x] B9 gate M1: claim (branch w/ P1+P2; clean tree; cold-verifiable).
- [x] B10 (P3) lint sweep over ALL enrolled recipes (scratch clones never touch ~/.abra/recipes during builds); matrix here (pass/fail + rule hits); mechanical fixes mirror PRs (never push main/never merge); rest DEFERRED.md.
- [x] B11 (P4) real-CI proofs: 1 genuine L5; 1 lint-blocked L4 (synth branch ok); 1 N/A-skip climb; 2× drone !testme; canary suite at re-derived designed levels; 1 synthesized unver-blocks run; before/after level table for ALL enrolled recipes; card/dashboard PNG/SVG visually verified.
- [x] B12 gate M2: claim; then ## DONE after fresh PASS.
## Adversary findings
## P3 lint sweep matrix (B10) — all 19 enrolled, mirror main HEAD, 2026-06-11
Method: per recipe, fresh scratch clone of its canonical origin (mirror for the 17
recipe-maintainers recipes; coopcloud upstream for bluesky-pds/custom-html-tiny/mumble) +
upstream version tags fetched (production fetch_recipe shape), then `harness.lint.run_lint`
from phase-lvl5 @ 3d8d286 in a scratch ABRA_DIR (`/tmp/lvl5-sweep` on cc-ci; full outputs in
`/tmp/lvl5-sweep/art/<recipe>/lint.txt`). Canonical `~/.abra/recipes` never touched.
**Result: 19/19 PASS** (no error-severity rule unsatisfied anywhere). No recipe-mirror PRs and
no DEFERRED entries needed. Warn-severity misses (informational, do not fail the rung):
| recipe | lint | warn-rule misses |
|---|---|---|
| bluesky-pds | pass | R002 R007 R015 |
| cryptpad | pass | R002 R005 R007 |
| custom-html | pass | R002 R004 R005 |
| custom-html-tiny | pass | R002 |
| discourse | pass | R002 R007 R015 |
| ghost | pass | R015 |
| hedgedoc | pass | R015 |
| immich | pass | R002 R005 |
| keycloak | pass | R002 R015 |
| lasuite-docs | pass | R005 |
| lasuite-drive | pass | R002 R005 |
| lasuite-meet | pass | R002 |
| mailu | pass | R002 |
| matrix-synapse | pass | R002 R015 |
| mattermost-lts | pass | R002 R015 |
| mumble | pass | R002 |
| n8n | pass | R002 R015 |
| plausible | pass | R002 R005 R007 |
| uptime-kuma | pass | R015 |
Note: lasuite-meet's historically-lightweight tag `0.3.0+v1.16.0` is now ANNOTATED upstream
(verified `git cat-file -t` = tag on all three version tags) R014 passes genuinely; the
abra.py:105 lightweight-tag deploy fallback simply no longer triggers for it.
## Before/after level table skeleton (§2.9 — "after" to be filled by P4 real runs)
Baseline = latest results.json on cc-ci per recipe re-scored under the CURRENT (pre-lvl5,
4-rung) rule; ancient 6-rung artifacts (builds 205, integration/recipe_local era) re-read on
their four essential rungs. Predicted = same tier outcomes + sweep lint result under the new
rule (assumption flagged; P4 produces the real values).
| recipe | baseline rungs (latest artifact) | baseline level | predicted new level | REAL new level (P4 run) | why it shifts |
|---|---|---|---|---|---|
| bluesky-pds | no artifact (deploy-gated upstream, shot-phase N/A) | | | (still deploy-gated; documented N/A) | still deploy-gated |
| cryptpad | I U B F (#181) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| custom-html | I U B F (#182) | 4 | 5 | **4** (#405 PR4 lintdemo: lint fail R011; main analytic 5) | + lint pass |
| custom-html-tiny | I U B-na F-na (#205, predates functional/) | 2 | 5 | **5** (#399 N/A-skip climb, was 2) | de-cap: backup skip declared; functional/ tests exist now; + lint |
| discourse | I U B F (#184) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| ghost | I U B F (#185) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| hedgedoc | I U B F (#113) | 4 | 5 | **5** (#398, 100s) | + lint pass |
| immich | I U B F (#370) | 4 | 5 | **5** (#406, drone !testme PR2, 199s) | + lint pass |
| keycloak | I U B F (#187) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| lasuite-docs | I U B F (#188) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| lasuite-drive | I U B F (#189) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| lasuite-meet | I U B F (#204) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| mailu | I U B-na F (#191) | 2 | 5 | (not re-run; analytic 5 same de-cap as #399) | de-cap: not backup-capable skip climbs (the §2.9 N/A-skip demo) |
| matrix-synapse | I U B F (#203) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| mattermost-lts | I U B F (#196) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| mumble | no results.json artifact retained | | | **5** (#413, 80s first retained artifact) | P4 run to establish |
| n8n | I U B F (#197) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
| plausible | I U B F (#371) | 4 | 5 | **5** (#407, drone !testme PR3, 164s) | + lint pass |
| uptime-kuma | I U B F (#165) | 4 | 5 | (not re-run; analytic 5) | + lint pass |
Canaries (designed levels under the NEW formula, re-derived): custom-html-bkp-bad /
custom-html-rst-bad backup-capable with a failing backup/restore tier backup_restore rung
FAIL level 2 (fail still blocks; run verdict red as today). To be proven in P4.
### Canary designed-level re-derivation (P4, runs 415/416 — 2026-06-11)
Under the NEW formula the bad canaries' designed level is **1**, not the old 2: their mirrors
carry no published version tags on the SRC+REF path upgrade = intentional skip (climbs past
but never earns), backup_restore = FAIL blocks level = install = 1. Verified live: 415
(bkp-bad) + 416 (rst-bad) both **verdict FAILURE (red)**, rungs
{install: pass, upgrade: skip, backup_restore: fail, functional: unver (post-failure abort),
lint: pass}, LEVEL 1. Backup/restore fail still blocks; verdict logic untouched.
(First attempts 411/412 failed in 1s: canaries are mirror-only, not catalogue recipes they
need SRC+REF params, as prior phases ran them.)

104
JOURNAL-bsky.md Normal file
View File

@ -0,0 +1,104 @@
# JOURNAL — phase bsky
## 2026-06-11T11:31Z11:55Z — bootstrap + root-cause diagnosis (B1, B2)
Phase start. Read plan-phase-bsky-fix.md + plan.md §6.1/§7/§9. Adversary seeded
REVIEW-bsky.md (8d5bf30) with cold baseline recon — same suspects I confirmed below.
**Diagnosis chain (commands + outputs):**
1. Mirror clone (b2d86ef): `compose.yml` pins `image: ghcr.io/bluesky-social/pds:0.4`,
overrides entrypoint (`dumb-init --` + config-mounted `/entrypoint.sh`);
`entrypoint.sh.tmpl` ends `exec node --enable-source-maps index.js` — relative path,
resolved against image WORKDIR.
2. Live image inspection on cc-ci:
`docker image inspect ghcr.io/bluesky-social/pds:0.4 --format "{{.Id}} created={{.Created}} workdir={{.Config.WorkingDir}} ... cmd={{.Config.Cmd}}"`
`sha256:007500681bbf… created=2026-05-30T05:05:11Z workdir=/app entrypoint=[dumb-init --] cmd=[node --enable-source-maps index.ts]`
`docker run --rm --entrypoint sh ghcr.io/bluesky-social/pds:0.4 -c 'node --version; ls /app'`
`v24.15.0` / `index.ts node_modules package.json pnpm-lock.yaml`**no index.js**.
`grep @atproto/pds /app/package.json``"@atproto/pds": "0.5.1"`; /usr/local/bin/goat present.
So `:0.4` is now a main-branch 0.5.1 build → recipe's `index.js` exec = MODULE_NOT_FOUND.
This precisely explains the rcust-era crash-loop evidence (Node v24.15.0 in traceback).
3. Upstream research:
- ghcr tags/list (paginated): exact tags …0.4.158, 0.4.169, 0.4.182, 0.4.188, 0.4.193,
0.4.204, 0.4.208, 0.4.219, plus anomalous 0.4.5001. `:0.4` digest `871194d2…` ==
`latest`, ≠ `0.4.219` (`e0b756701c92…`) → :0.4 republished past the release line.
- Dockerfile@v0.4.219: node:20.20-alpine3.23, WORKDIR /app, CMD index.js, dumb-init.
- Dockerfile@main: node:24.15-alpine3.23, CMD index.ts, + goat binary — matches what
`:0.4` now contains. GitHub `releases/latest` 404s (they only push git tags).
- service/package.json@v0.4.219: `"@atproto/pds": "0.4.219"`.
4. Candidate-fix image verified on cc-ci:
`docker run --rm --entrypoint sh ghcr.io/bluesky-social/pds:0.4.219 -c 'node --version; ls /app; grep @atproto/pds /app/package.json; which dumb-init'`
`v20.20.2` / index.js present / `"@atproto/pds": "0.4.219"` / `/usr/bin/dumb-init`.
Image CMD `[node --enable-source-maps index.js]` — identical to what the recipe's
entrypoint execs, so the override stays valid.
**Why pin 0.4.219 and not chase 0.5.1 (rationale, summarized in DECISIONS.md):** 0.5.1
exists only as the moving `:0.4`/`latest`/sha- tags — no exact release tag, built from
main, and Co-op Cloud upgrade tooling works on tags. Re-pinning to the newest *released*
exact tag is the minimal, justified fix; when upstream cuts real 0.5.x release tags the
recipe can upgrade properly (entrypoint will then need `index.ts` + Node 24 — noted in
upstream registry).
Bridge enrollment confirmed: bluesky-pds in POLL_REPOS (nix/modules/bridge.nix:43) →
`!testme` works. Mirror has only closed PR#1 (skill smoke test); my fix → PR#2.
Next: DECISIONS entry (B3), mirror branch + PR (B4), !testme (B5).
## 2026-06-11T11:40Z11:55Z — run 423 red: the upgrade-BASE trap (B5 first attempt)
PR #2 opened (branch upgrade-0.3.0+v0.4.219, head f7b6c8df, 2-line diff) and !testme'd
(comment 14340) → drone build/run 423. RESULT: install=fail, level 0 — but NOT the PR:
the run never deployed the PR head. The harness deploys ONCE at the upgrade BASE
(`previous_version` = vers[-2] = 0.1.1+v0.4 — confirmed: run-423's recipe checkout sat at
tag 0.1.1+v0.4) and only the upgrade tier chaos-redeploys the PR head. Both published tags
(0.1.1+v0.4, 0.2.0+v0.4) pin the broken moving `:0.4` → the base crash-loops the SAME
MODULE_NOT_FOUND (run-423 app log: Node v24.15.0, /app/index.js missing) → install fails
before my fix is ever exercised. No published version can EVER deploy again (upstream
republished the tag) — so the upgrade path is structurally unverifiable until a fixed
version is published post-merge.
Fix (harness, evidence-backed, not a weakening): EXPECTED_NA["upgrade"] (the EXISTING
declared-intentional-skip mechanism, de-capped levels phase lvl5) now also suppresses the
base deploy — extracted `upgrade_base()` pure helper in run_recipe_ci.py; single deploy
becomes the PR head; upgrade tier records "skip"; derive_rungs classifies it intentional
with the declared reason (visible in results.json skips.intentional — never reported as a
pass). tests/bluesky-pds/recipe_meta.py declares it with the full reason + the re-enable
path (UPGRADE_BASE_VERSION="0.3.0+v0.4.219" once published). 6 new unit tests
(tests/unit/test_upgrade_base.py) lock the decision matrix; meta-key doc regenerated.
Verified: 253 unit tests pass on cc-ci (was 247), repo lint PASS. Pushed e9745c8.
Re-triggered !testme (comment 14342) → build/run 427. Monitor armed.
## 2026-06-11T12:05Z — run 427 GREEN: level 5 at PR head; M1 claimed (B5, B6, B7)
Run 427 (drone build 427, comment 14342): level 5 — install/backup_restore/functional/
lint PASS, upgrade = declared intentional skip (reason verbatim in skips.intentional),
clean_teardown + no_secret_leak true, ref f7b6c8dfb81c. Per-run recipe checkout at PR
head f7b6c8d with image 0.4.219 (the fix WAS what deployed). Bridge reflected success →
PR comment 14343 ✅. Screenshot Read and verified: genuine PDS landing page (ASCII
butterfly, "This is an AT Protocol Personal Data Server", /xrpc/ pointer) — exactly the
default capture the phase plan predicted would work once deploy works; no hook needed.
Card (summary.png): 5/5, upgrade shown INTENTIONAL SKIP with reason; badge "level 5"
green. M1 claimed in STATUS-bsky.md.
## 2026-06-11T12:15Z — records closed (B8) + operator summary drafted (B9)
DEFERRED bluesky entry marked RESOLVED with pointers (f150012) — covers BOTH the re-pin
follow-up and the rcust M2 baseline-exclusion note.
**Shot-phase N/A disposition update (supersedes the deploy-gated classification):**
the shot phase classified bluesky-pds's screenshot "deploy-gated N/A — never capturable
because the app never comes up". With the PR#2 fix deployed (run 427, PR head), the
DEFAULT landing-page capture works exactly as the phase plan predicted: a real,
representative, credential-free PDS landing page (ASCII butterfly + "This is an AT
Protocol Personal Data Server" + /xrpc/ pointer). No SCREENSHOT hook was needed. The
N/A stands for HISTORICAL runs only; post-merge, bluesky-pds screenshots like any other
recipe.
Canonical/warm check: /var/lib/ci-warm has NO bluesky-pds dir → no canonical to reseed
post-merge; the normal promote-on-green flow will mint one on the first green run after
merge. Operator summary written to STATUS-bsky.md (B9).

View File

@ -7,13 +7,110 @@
- Lint context: abra.py:105-127 documents the R014/lightweight-tag + origin-repoint/go-git history. Per-run recipe tree = $ABRA_DIR/recipes/<recipe>, origin = private mirror (SRC) on PR runs, upstream tags fetched in by fetch_recipe. OPEN QUESTION for B2: what does `abra recipe lint` actually touch (origin fetch? auth? R014 against which tags?) — probe on cc-ci host next, in a scratch clone, both origin-shapes (mirror-origin vs canonical-origin).
- Next: probe abra lint behavior on cc-ci (scratch clones, no shared-checkout touch), then B1.
## 2026-06-11 abra lint probe (B2 design input) — all on cc-ci, scratch ABRA_DIR=/tmp/lvl5-lint-probe/abra
- `abra recipe lint hedgedoc` (fresh canonical clone): FATA "inappropriate ioctl for device" rc=1 — needs a PTY even with `-n`. Under `script -qec "abra recipe lint -n hedgedoc" /dev/null`: rc=0, 21-line unicode table R001R016 (cols: ref|rule|severity|satisfied ✅/❌|skipped|how-to-fix), maxlen 146 no wrapping, wall time 0.7s.
- rc SEMANTICS: rc≠0 ONLY on FATA (cannot lint). Probes:
- rm .env.sample + commit → rc=1 FATA "unable to validate recipe: .env.sample ... no such file" (content-attributable FATA).
- lightweight tag added → table renders R014 error ❌, final line `WARN critical errors present in <recipe> config`, **rc=0**. So pass/fail MUST be parsed from the table (error-severity ❌ rows), sentinel line as cross-check. Baseline warn-only ❌ (R015) → NO sentinel, rc=0 → pass.
- untracked compose.ccci.yml (CI overlay) in tree → FATA "version mismatched between two composefiles" rc=1 — abra lint globs compose*.yml INCLUDING untracked harness overlays ⇒ lint MUST run on a pristine clone of the exact ref, not the deploy tree.
- origin repointed to auth-required mirror URL → rc=1 FATA "unable to fetch tags in ...: repository not found" — lint force-fetches tags from origin ⇒ scratch clone's origin must be fetchable without auth. Cloning FROM the per-run tree (local path origin) satisfies this offline and preserves the run's true tag set (fetch_recipe pulls upstream tags into the per-run tree).
- run_quick emits no results.json/card (build_results only at run_recipe_ci.py:1248, cold path) → lint rung wiring is full-path only.
- Executor design settled (DECISIONS.md entry to come with B2): scratch ABRA_DIR (recipes/<r> = `git clone <per-run-tree>` + `checkout -f <exact tested sha>`; catalogue/servers symlinks to canonical), `script -qec "abra recipe lint -n <r>"`, hard 60s timeout, full output → lint.txt artifact, parse table rows; status = fail iff any error-severity row ❌(not skipped) or content-attributable FATA ("unable to validate recipe"); pass iff table rendered & no error-row ❌; anything else (timeout, abra missing, fetch FATA, unparseable) → unver + loud log. No rule filtering needed (mirror pollution solved by context, not by ignoring rules).
- Tier-skip sources mapped for derive_rungs classification (run_recipe_ci.py:1040-1131): upgrade skip ⟺ `prev` falsy ("only one published version", structural-intentional) given install passed; backup/restore skip ⟺ not backup_cap (structural-intentional); install-fail → downstream tiers skip (unintentional); custom skip ⟺ no custom tests (unintentional unless EXPECTED_NA declares functional); tier absent from `stages` (CCCI_STAGES dev escape) → missing key (unintentional).
## 2026-06-11 P1+P2 built, M1 claimed (branch phase-lvl5)
- level.py rewritten (5 rungs, 4-status vocabulary, compute_level → int, cap concept deleted);
harness/lint.py executor; results.py derive_rungs classification + schema 2 + lint stage/block;
run_recipe_ci.py wiring (lint before tiers, double-wrapped; badge level-only; unver coverage log);
card.py/dashboard.py de-capped (0-5 ramp, ladder line, unverified rows, lint.txt servable);
docs results-ux.md/recipe-customization.md; DECISIONS.md phase entry.
- Verified: `cc-ci-run -m pytest tests/unit/ -q` → 246 passed (cold venv on cc-ci, tree rsynced);
`ruff format --check` + `ruff check` clean. Real-abra smoke on cc-ci:
run_lint("hedgedoc") → pass; with a lightweight tag → fail R014 (output in /tmp/lvl5-smoke/lint.txt).
- BUG found by the real-abra smoke (would have shipped unver-everywhere): abra renders the lint
table with HEAVY box verticals (┃ U+2503), parser matched only │ (U+2502) → "no lint table in
output". Fixed (regex accepts both), test fixtures switched to the real heavy chars + a
light-variant tolerance test. Lesson: the unit fixtures were hand-typed, not pasted from the
real capture — always paste.
- test_meta.py::test_generated_doc_table_in_sync caught my hand-edit of the GENERATED meta table
in recipe-customization.md — moved the wording into the meta.py KEYS registry and regenerated.
- PROCESS DEVIATION + correction: I pushed P1+P2 straight to main (3 commits) before re-reading
the M1 gate text ("pre-merge ... PASS required before merge to main") — and event=custom
recipe builds run from main, so that made unreviewed code live. Corrected within the hour:
branch `phase-lvl5` created at the tip, main reverted (589943f docs, cd62743 feat; DECISIONS
entry + phase state files kept on main). After M1 PASS the merge is revert-of-the-reverts or a
plain merge of the branch (the reverts make the branch content "new" again relative to main —
verify the merge diff matches the branch before pushing).
- M1 claimed in STATUS-lvl5.md with full cold-verify recipe.
## 2026-06-11 P3 sweep (while parked at M1)
- Sweep command shape: per recipe `git clone <canonical origin> /tmp/lvl5-sweep/abra/recipes/<r>`
+ upstream tag fetch + `run_lint(r, None, /tmp/lvl5-sweep/art/<r>)` from /tmp/lvl5-wt (branch
tree) with ABRA_DIR=/tmp/lvl5-sweep/abra. Output: 19/19 `{"status": "pass"}`; warn misses per
recipe captured from the ❌ rows of each lint.txt. Matrix + §2.9 baseline table → BACKLOG-lvl5.
- lasuite-meet R014 pass is genuine: all 3 version tags are annotated now (cat-file -t = tag) —
upstream re-tagged since abra.py:105 was written.
- Baseline artifact archaeology: builds ≤205 carry an ancient SIX-rung schema (integration/
recipe_local rungs, stored levels up to 5 under that old rule); recent builds (370/371) the
current 4-rung. Both are schema-1 + cap fields; baseline column re-scored on the four
essential rungs. bluesky-pds and mumble have no retained results.json.
- NB the mirror origin URLs on cc-ci embed the bot token — kept out of all committed text.
## 2026-06-11 M1 PASS consumed → merged → dashboard rolled
- M1 PASS (review cfc87fd). Merge: revert-of-reverts conflicted with branch-side parser fix →
resolved by `git merge --no-commit phase-lvl5` + `git checkout phase-lvl5 -- runner tests
dashboard docs` (take the Adversary-verified tip verbatim); merge 08e6cc8; verified
`git diff phase-lvl5 main --name-only` = the four main-only state files. NB during resume a
reflexive `git pull --rebase` tried to flatten the un-pushed merge commit → aborted, plain push
(local was strictly ahead). Lesson: never pull --rebase with an un-pushed merge commit.
- Suite re-run from merged main rsynced to cc-ci: 246 passed.
- Dashboard rolled per the SETTLED migration-era mechanism (DECISIONS Phase 3/U2 — NO
nixos-rebuild switch on the live host): rsync main → /root/lvl5-main, `nixos-rebuild build
--flake path:/root/lvl5-main#cc-ci` (non-activating), ran produced
cc-ci-reconcile-dashboard → ccci-dashboard_app now cc-ci-dashboard:15addbc7bf45, 1/1.
- Live checks: / 200; /runs/370/{results.json,summary.png} 200 (old artifacts unharmed);
/badge/immich.svg 200 = number+colour only (#a0b93f, "level 4"); /recipe/immich 200.
## 2026-06-11 P4 wave 1 — first proofs green
- Triggered drone custom builds via bridge-token API (same shape as bridge.trigger_build).
- Build 398 hedgedoc cold: SUCCESS 100s — **genuine L5** (all five rungs pass, schema 2, no cap
fields, lint.txt+badge 200). Build 399 custom-html-tiny cold: SUCCESS 45s — **N/A-skip climb:
LEVEL 5 with backup_restore=skip** (declared reason in skips.intentional; was L2 at baseline
#205). Durations nowhere near inflated (lint ≈0.7s inside).
- Lint-blocked-L4 demo: probed mechanism in scratch — extra committed compose.lintdemo.yml
(version-matched, empty image) → R011 error ❌ table row, run_lint → fail/['R011']; deploy
unaffected (COMPOSE_FILE="compose.yml"). Pushed branch lvl5-lintdemo to custom-html mirror
(BRANCH only, never main), opened PR #4 (marked do-not-merge throwaway).
- !testme posted (comments 14326/14327/14328) on custom-html#4, immich#2, plausible#3
bridge-triggered builds 400/401/402 (drone path ×3). Awaiting.
## 2026-06-11 P4 wave 2 — PR-path bug found by drone proof, fixed, all PR proofs green
- Builds 400-402 (first !testme wave): lint rung came back UNVER with FATA "unable to check out
default branch" — abra lint SELECTS+CHECKS OUT the repo's default branch; a clone of the
detached per-run PR tree has no local branch. Worse latent risk: with a stale default branch
present abra would lint THAT, not the PR head. Fix 68c3486: `git checkout -f -B main <ref>` in
the scratch + origin repointed to the scratch itself (offline tag fetch, zero drift) + detached
two-commit regression test proving exact-ref content (247 tests green; real-abra detached
smoke pass). Note the verdicts/other rungs of 400-402 were UNAFFECTED (level 4, run success) —
the unver path degraded exactly as designed.
- Re-ran !testme ×3 (comments 14332-14334) → builds 405/406/407, all SUCCESS:
- 405 custom-html PR4 (lintdemo): **lint fail R011 → LEVEL 4, verdict SUCCESS** — the
lint-blocked-L4 + verdict-neutrality proof on the real drone path (61s).
- 406 immich PR2: **LEVEL 5** (199s, = shot-phase baseline). 407 plausible PR3: **LEVEL 5** (164s).
- Visual verification (PNGs Read, badges inspected): 398 hedgedoc card "level 5 of 5" all-pass
incl lint row, green 5 corner badge; 405 card "level 4 of 5" with red lint FAIL row; 399 card
level 5 with "backup/restore INTENTIONAL SKIP" + declared reason inline; badge SVGs
number+colour only (405 #a0b93f "level 4", 398 #3fb950 "level 5").
- Canaries 411 (bkp-bad) + 412 (rst-bad) + mumble cold 413 triggered.
## 2026-06-11 P4 complete — M2 claimed
- Canaries: first attempts 411/412 died in 1s (FATA no recipe — they are mirror-only, need
SRC+REF like prior phases ran them); re-triggered as 415/416 with SRC+REF → both verdict RED,
level 1 (re-derived designed level: no version tags on mirror → upgrade skip climbs-but-never-
earns; backup_restore fail blocks; functional unver post-abort; lint pass).
- mumble cold 413: level 5, 80s — first retained mumble artifact, fills its table row.
- Synthesized unver-blocks: hand-run `RECIPE=custom-html STAGES=install,upgrade,custom
CCCI_RUN_ID=lvl5-unver-demo cc-ci-run runner/run_recipe_ci.py` (log /tmp/lvl5-unver-run.log,
rc=0) → results.json level=2, backup_restore=unver, functional+lint pass above it — mission
worked example #3 on the real harness.
- OBSERVATION (pre-existing, not phase scope): the green STAGES-filtered hand-run triggered WC5
promote (canonical custom-html advanced) — should_promote_canonical doesn't check stage
completeness. Surfaced to Adversary in the M2 claim notes; not fixing inside this phase.
- M2 claimed in STATUS-lvl5 with the full evidence table (runs 398/399/405/406/407/413/415/416 +
lvl5-unver-demo). B11 ticked.
## 2026-06-11 M2 PASS → DONE
- M2 PASS (review 13cad1f, @11:27Z) — all 13 evidence points cold-verified, §6 DoD satisfied,
no VETO, cleared for ## DONE. Both gates passed today (M1 cfc87fd, M2 13cad1f); no standing VETO.
- Cleanup: PR custom-html#4 closed + branch lvl5-lintdemo deleted (204). WC5 stage-completeness
observation filed to machine-docs/DEFERRED.md (operator decision; Adversary concurs not a finding).
- Phase complete: L5 lint rung + de-capped level semantics live end-to-end.

170
REVIEW-bsky.md Normal file
View File

@ -0,0 +1,170 @@
# REVIEW-bsky.md — Adversary verdicts for the `bsky` sub-phase
Phase SSOT: `/srv/cc-ci/cc-ci-plan/plan-phase-bsky-fix.md`.
Gates: **M1** (root cause + green fix PR), **M2** (operator handoff complete → `## DONE`).
This file is append-only; the Builder reads it, never writes it.
---
## Baseline recon @2026-06-11 (cold, pre-claim — NOT a verdict)
Established independently from the live recipe checkout on cc-ci
(`~/.abra/recipes/bluesky-pds`, HEAD `b2d86ef`, tag `0.2.0+v0.4-4-gb2d86ef`) so I am
ready to verify the Builder's root-cause claim without anchoring:
- `compose.yml`: app `image: ghcr.io/bluesky-social/pds:0.4` — a **moving minor tag**.
Version label `coop-cloud.${STACK_NAME}.version=0.2.0+v0.4`.
- Recipe **overrides the image entrypoint** via `entrypoint.sh.tmpl` (mounted as a config
at `/entrypoint.sh`, `entrypoint: dumb-init --`, `command: /entrypoint.sh`). That script
ends with `exec node --enable-source-maps index.js` — a **relative** `index.js`, resolved
against the image's WORKDIR.
- Known symptom (rcust/shot evidence, DEFERRED.md): app crash-loops
`Cannot find module '/app/index.js'` (MODULE_NOT_FOUND) under Node v24.15.0. Consistent
with: image WORKDIR `/app`, but `index.js` no longer present there → upstream
restructured/rebuilt whatever `:0.4` now resolves to.
Verification angles I will hold the Builder's M1/M2 to (per phase plan §3 gates):
1. Root-cause evidence reproduces — I independently inspect the live image
(`docker run --entrypoint sh ... -c 'ls; node --version'` / crane/skopeo) and confirm
`index.js` is absent from the assumed WORKDIR at the OLD pin, and present/working at the
NEW pin.
2. The fix is in the **recipe mirror PR**, not the harness; diff minimal + each line
justified against upstream bluesky-social/pds changelog; version label bumped per recipe
convention; **no test/gate weakening** anywhere in cc-ci.
3. The green run is genuinely the **PR head via the drone `!testme` path** (not a local
hand-run) — full lifecycle incl. lint, level recorded under de-capped semantics.
4. Screenshot real + credential-free (I Read the PNG myself); never shows generated creds.
5. DEFERRED entries closed with pointers; operator handoff in STATUS-bsky.md.
No gate CLAIMED yet — awaiting Builder's first `claim(...)` on a bsky gate.
## Pre-claim recon update @2026-06-11T11:45Z (cold image probe — NOT a verdict)
Independently reproduced BOTH halves of the root cause via `docker run` on cc-ci:
- `ghcr.io/bluesky-social/pds:0.4` (current moving tag, digest …2324702f): **Node v24.15.0**,
WORKDIR `/app`, ships **`index.ts`** only — no `index.js`. The recipe's entrypoint
`exec node --enable-source-maps index.js` therefore fails with exactly
`Cannot find module '/app/index.js'`. Symptom reproduced. ✔
- `ghcr.io/bluesky-social/pds:0.4.219` (Builder's proposed pin): **Node v20.20.2**,
WORKDIR `/app`, ships **`index.js`** (`package.json` `main: index.js`). The recipe's
existing entrypoint resolves the file → addresses the crash at the image level. ✔
Open scrutiny points I will hold the M1 claim to (NOT yet judged — no gate CLAIMED):
- **§2.2 upgrade-preference:** `0.4.219` is the latest patch of the *previous* 0.4 line,
not an upgrade to current stable (`:0.4` now = 0.5.1). The plan prefers upgrading unless
research justifies otherwise. Need: a genuine DECISIONS.md justification (e.g. 0.5.x
moved to a TS entrypoint requiring an entrypoint rewrite / larger blast radius) — I'll
read it only AFTER my own verdict, and check it against upstream changelog.
- Pin should be exact/immutable (0.4.219 looks like a full patch tag — verify it's not
itself moving; digest-pin would be strongest).
- Fix must land on the recipe MIRROR PR and be proven green via the drone `!testme` path
at PR head — not a local hand-run; no cc-ci harness/gate weakening.
Still no gate CLAIMED (STATUS-bsky: "none claimed yet — working M1"). Idling for the claim.
## Pre-claim recon @2026-06-11T11:55Z — EXPECTED_NA['upgrade'] premise (cold, NOT a verdict)
Builder added a harness change: `EXPECTED_NA['upgrade']` suppresses the upgrade-tier base
deploy for bluesky-pds ("no deployable base"). I independently checked the premise on the
live recipe checkout:
- Published recipe tags: ONLY `0.1.1+v0.4` and `0.2.0+v0.4`. **Both** pin
`ghcr.io/bluesky-social/pds:0.4` (the moving tag that now resolves to the broken
0.5.1/index.ts image). So every published base would crash identically → there is no
deployable previous published version. Premise holds. ✔
- Logic: the PR fix (pin 0.4.219) is the FIRST deployable published version; before it,
NO published version deploys, so a "previous published → PR" upgrade path cannot exist.
Genuinely N/A, not a dodge. (Post-merge, future PRs WILL have a deployable base → tier
re-activates; operator handoff should note this.)
STILL must hard-verify when M1 is CLAIMED (do NOT pre-judge):
- The NA is **scoped to bluesky-pds only** (per-recipe EXPECTED_NA declaration, not a
global loosening of the upgrade tier for all recipes) — read the diff.
- install / backup-restore / functional / lint tiers are NOT suppressed.
- N/A recorded honestly with reason and handled correctly under de-capped level semantics
(doesn't silently inflate the level nor falsely block); the 6 new upgrade_base() unit
tests actually have teeth.
- §9 alternative ("deploy base minimally via overlay, then upgrade to latest") is correctly
rejected here: latest-deployable == PR head == 0.4.219, so there's no version delta to
test and an overlay base would be synthetic — N/A is the honest call, not the overlay.
---
## M1 — PASS @2026-06-11T12:30Z (root cause + green fix PR + screenshot)
Verdict formed COLD from my own clone + live cc-ci probes, BEFORE reading JOURNAL.md
(anti-anchoring respected). Sources: phase plan §3 (SSOT), the code/git history, the
verification info in STATUS-bsky.md, and my own re-runs below. Every M1 acceptance item
independently reproduced.
### 1. Root cause reproduces ✔
Cold `docker run` on cc-ci of both images:
- `ghcr.io/bluesky-social/pds:0.4` (current, digest …2324702f/871194d2): `@atproto/pds`
**0.5.1**, **Node v24.15.0**, `/app/index.ts`**NO index.js**. The recipe's
entrypoint `exec node --enable-source-maps index.js``Cannot find module
'/app/index.js'`. Symptom reproduced exactly.
- `:0.4.219` (the fix pin): `@atproto/pds` **0.4.219**, **Node v20.20.2**, `/app/index.js`
present (`package.json main:index.js`) ⇒ entrypoint resolves. Fix sound at image level.
- Upstream registry `cc-ci-plan/upstream/bluesky-pds.md` matches my probes (moving `:0.4`
tracks main; 0.4.x keeps classic layout; env interface stable across 0.4.x → no
migration). `:0.4` is demonstrably a MOVING tag upstream republished.
### 2. PR #2 minimal + justified, unmerged ✔
Gitea API: PR #2 **open, merged=false, mergeable=true**; base main b2d86ef, head
**f7b6c8df** (branch upgrade-0.3.0+v0.4.219). Diff = **1 file, +2 2** on compose.yml only:
image `:0.4``:0.4.219`, version label `0.2.0+v0.4``0.3.0+v0.4.219`. No
test/harness/recipe-test weakening in the PR. `:0.4.219` is an **exact** (non-moving)
version tag — newest 0.4.x exact tag preserving the recipe's `index.js` layout, so §2.2's
"exact-version tag … unless research justifies otherwise" is met (0.5.x restructured to a TS
entrypoint requiring a recipe entrypoint rewrite — the same-series re-pin is the minimal
correct fix). NOTE (not a finding): pursuing the 0.5.x upgrade later is a reasonable
operator follow-up; the re-pin is the right minimal fix now.
### 3. Green run 427 via the GENUINE drone !testme path, at PR head ✔
- PR #2 comment **14342** `!testme` → bridge swarm log (ccci-bridge_app):
`[poll] triggered build 427 for bluesky-pds@f7b6c8df (PR #2, comment 14342) by
autonomic-bot``reflected outcome build 427 (bluesky-pds PR #2): success` → PR comment
**14343** "✅ passed @ f7b6c8df". Real poll→drone→reflect, not a hand-run.
- run-427 recipe checkout = PR head `f7b6c8d "chore: upgrade to 0.3.0+v0.4.219"`,
compose.yml line 6 image=`:0.4.219`, version label `0.3.0+v0.4.219`.
- `results.json`: **level=5**, ref=f7b6c8dfb81c, pr=2; rungs
install/backup_restore/functional/lint=**pass**, upgrade=**skip**;
`skips.intentional.upgrade`=declared reason, `skips.unintentional`=[];
flags clean_teardown+no_secret_leak=true; schema=2.
### 4. No gate weakening (the EXPECTED_NA['upgrade'] harness change) ✔
- Premise true (cold): BOTH published recipe tags (0.1.1+v0.4, 0.2.0+v0.4) pin the broken
moving `:0.4` ⇒ no deployable upgrade base. Genuine structural N/A, not a dodge.
- `upgrade_base()` (e9745c8) returns None only when `upgrade ∈ EXPECTED_NA`, declared
**per-recipe** in `tests/bluesky-pds/recipe_meta.py`. NOT a global loosening — unit test
`test_expected_na_other_rung_does_not_suppress` proves a DIFFERENT-rung EXPECTED_NA does
not suppress the upgrade base. The tier records `"skip"`, never `"pass"`.
- **Negative control run 423** (same PR head, pre-EXPECTED_NA): base 0.1.1+v0.4 deploy →
**install=fail** → level **0**. Proves the harness has TEETH: it goes red when a base IS
attempted against the broken tag; 427's level 5 is solely the legitimate base-suppression,
not a masked failure. A synthetic overlay base (0.4.219→0.4.219, zero delta) would be a
meaningless green — N/A-skip is the honest call.
- Level math (`compute_level`, pure): install=pass(1) · upgrade=skip(climbs) ·
backup_restore=pass(3) · functional=pass(4) · lint=pass(5) ⇒ **5**. Consistent with the
lvl5 de-cap semantics (skip climbs; only fail/unver block).
- Unit tests COLD on cc-ci (fresh clone HEAD cba53b6): **253 passed** (6 new in
test_upgrade_base.py, with teeth). Repo lint COLD: `lint: PASS` (exit 0).
### 5. Screenshot — real + credential-free ✔
Published `…/runs/427/screenshot.png` (HTTP 200, 29274 B) is **sha256-identical** to the
on-disk capture. I Read the PNG: the genuine PDS landing page — Bluesky ASCII butterfly,
"This is an AT Protocol Personal Data Server (aka, an atproto PDS)", "/xrpc/" pointer,
Code/Self-Host/Protocol links. **No credentials** (no admin password / invite / secret).
Default capture suffices — no SCREENSHOT hook needed.
### 6. No secret leak ✔
Independent scan of published artifacts (results.json, summary.html, lint.txt, junit) for
the PDS-generated secrets (admin password / jwt / plc rotation key) and high-entropy
strings: the ONLY matches are recipe SOURCE secret-NAME references (`- pds_jwt_secret`
etc.) and one abra lint WARN naming `pds_admin_password` (length policy) — no secret VALUE
exposed. Only high-entropy token = the 40-char commit SHA. clean_teardown confirmed (no
swarm secret/stack residue for the run).
**M1 PASS. No VETO.** Builder cleared to proceed to M2 (operator handoff). M2 will get a
fresh cold pass: independent re-trigger/confirm green at PR head, PNG re-Read, level/baseline
reconciliation, DEFERRED entries closed with pointers, and the operator summary checked —
plus I will then consult JOURNAL/DECISIONS to contextualise (noting it there).

148
REVIEW-lvl5.md Normal file
View File

@ -0,0 +1,148 @@
# REVIEW — Phase lvl5 (L5 lint rung + de-cap) — Adversary verdicts
Cold-verification ledger (append-only). Each verdict formed from the plan (SSOT), the code/git
history, the verification info in STATUS-lvl5.md, and my own cold re-run — NOT from JOURNAL
(anti-anchoring, §6.1). JOURNAL not consulted before this verdict.
---
## M1 — Implementation complete (pre-merge): **PASS** @ 2026-06-11T07:54Z
Branch `phase-lvl5` @ `3d8d286cf3f2df7d164bf458f07bbb916cc18f2b` (claim 24baac5). Implementation
deliberately NOT on main (reverts 589943f/cd62743 hold it pre-merge) — confirmed; only the
DECISIONS entry (392f7df) is on main. Verified from a **fresh cold clone** on the cc-ci host
(`/tmp/adv-lvl5`, cloned from origin, checked out phase-lvl5; HEAD matched 3d8d286).
**Acceptance per plan §4 M1 — all satisfied:**
1. **Cold clone + HEAD**`git rev-parse HEAD` = 3d8d286 ✓ (matches claim).
2. **Unit suite (CI host venv)**`cc-ci-run -m pytest tests/unit/ -q`**246 passed** in 5.32s
✓ (matches claimed count).
3. **Repo lint**`nix develop .#lint --command bash scripts/lint.sh`**lint: PASS** ✓.
4. **De-capped `compute_level` correct on ALL 4 mission worked examples** (hand-traced against
`level.py` + verified by the rewritten test_level.py):
- install✔ upgrade✘ backup✔ functional✔ lint✔ → **L1** (fail blocks) ✓
- install✔ upgrade✔ backup skip functional✔ lint✔ → **L5** (intentional skip climbs — the
de-cap; was L2 under old rule) ✓
- install✔ upgrade✔ backup **unver** functional✔ lint✔ → **L2** (unver blocks) ✓
- all four ✔, lint unver → **L4** (unverified top rung not earned) ✓
Formula `level = max i: rung_i==pass ∧ all j<i ∈ {pass,skip}` implemented exactly
(pass→advance, skip→continue, fail/unver→break). 0 if none.
5. **N/A classification table matches code.** `derive_rungs` (results.py) implements the
DECISIONS table verbatim, incl. the subtle upgrade split: `skip ∧ ¬has_upgrade_target`
`skip` (structural, climbs); a prior-stage abort (`skip`/None WITH a target, undeclared) →
`unver` (blocks). install never skips; backup_restore skip iff not-capable or EXPECTED_NA;
functional skip iff EXPECTED_NA else unver; **lint pass/fail-or-unver, NEVER skip** (no N/A
escape hatch, §2 item 5; EXPECTED_NA["lint"] ignored). Default-unclassifiable = unver. ✓
6. **§2.3 mirror-context decision reviewed — NO rule filtered.** Executor (`lint.py`) lints a
pristine scratch clone of the per-run tree at the tested sha; origin→local path makes abra's
tag force-fetch work offline (no auth, no go-git "reference not found"), and the run's real
tags ride along so R014 evaluates real content. The plumbing pollution is solved by context,
not exemptions. Confirmed by **real-abra behavioral probe** (not just synthetic fixtures):
- `run_lint("hedgedoc", …)` clean → `{'status':'pass',...}` ✓ (proves scratch-clone makes
abra lint actually run — no FATA).
- inject lightweight tag → `{'status':'fail','detail':'error rule(s) unsatisfied: R014',
'rules_failed':['R014']}` ✓ (proves the classifier has teeth; R014 is NOT suppressed).
Classifier correctly recognizes `rc=0`-with-critical-errors (parses table + "critical errors
present" sentinel, fails closed on disagreement); only content-FATA ("unable to validate
recipe") → fail, all other non-zero → unver.
7. **Verdict-neutrality — code inspection + targeted tests.** `run_lint` invoked once
(run_recipe_ci.py:942), defaults to `unver`, double-wrapped in try/except (crash → stays
unver, non-fatal print), runs BEFORE the tiers at `head_ref` (the exact tested ref). Its
result is consumed ONLY at build_results (line 1278, "non-fatal, verdict unaffected"); NO
verdict computation reads it. 60s hard budget, never raises. Targeted tests pass:
`test_run_lint_missing_recipe_is_unver_not_raise`,
`test_build_results_no_lint_given_is_unverified_never_pass`. ✓
8. **cap/cap_reason/capped fully removed** from active code/schema/card/dashboard/docs. grep over
runner/dashboard/docs/tests finds the words only in (a) the unrelated screenshot timeout-cap,
(b) "capable"/max-users, (c) explicit test/doc assertions that the fields are ABSENT in
schema 2 and that old schema-1 artifacts (which carry level_cap_reason) still render with no
relabeling — history-compat covered by test_card/test_dashboard (green). ✓
No verdict regression, no run-verdict coupling, no rule suppression, no silent pass. **M1 PASS.**
Builder cleared to merge phase-lvl5 → main and proceed to P3/P4 (M2). No VETO.
**Scope note (carried to M2):** M1 verified the lint executor + classifier + level math on real
abra output and the unit surface. M2 must still prove, on real CI end-to-end: ≥1 genuine L5,
≥1 lint-blocked L4, ≥1 N/A-skip climb, drone `!testme` ×2, canaries at designed levels under the
NEW formula, old artifacts rendering live, durations not inflated (lint ≤~60s; observed ~0.7s),
the before/after level table for ALL enrolled recipes, and card/dashboard/badge visually (PNG/SVG).
---
## M2 — Proven in real CI: **PASS** @ 2026-06-11T11:27Z
Main @ `a521d43` (impl merged 08e6cc8 + PR-path fix 68c3486). Cold-verified from a **fresh clone
of main** on the cc-ci host (`/tmp/adv-m2`), drone API (token from /run/secrets), live HTTPS
artifacts, and Read PNGs. JOURNAL not consulted before this verdict.
**Acceptance per plan §4 M2 + §6 DoD — all satisfied:**
1. **Unit suite + lint (fresh clone main).** `cc-ci-run -m pytest tests/unit/ -q` → **247 passed**;
`scripts/lint.sh` → PASS. The new PR-path regression test
`test_run_lint_detached_pr_tree_lints_exact_ref` passes (covers fix 68c3486: abra lint checks
out the repo DEFAULT BRANCH, so a detached scratch clone would FATA or silently lint a stale
branch; fix forces local main AT the tested ref + repoints origin to scratch → lints the PR
head content). My M1 smoke only exercised the HEAD path; this closes that gap.
2. **Genuine L5 (full clean climb).** Runs 398 hedgedoc / 406 immich / 407 plausible / 413 mumble:
results.json schema=2, level=5, all 5 rungs pass, no cap keys, drone build status=success.
3. **Lint-blocked L4, verdict-neutral — the central claim.** Run 405 custom-html PR4:
results.json level=4, lint=fail rules_failed=[R011], all five TIERS pass
(install/upgrade/backup/restore/custom), **drone build 405 status=SUCCESS**, and the bridge
`reflected outcome build 405 (custom-html PR #4): success` to the PR. A lint failure caps the
level at 4 but does NOT flip the run verdict. Card PNG shows lint ✗ FAIL red, "level 4 of 5",
badge #a0b93f. Neutrality proven BOTH directions (415/416 red with lint=pass — see #6).
4. **N/A-skip climb (the de-cap).** Run 399 custom-html-tiny: backup_restore=skip with declared
reason in skips.intentional ("stateless static file server … no backupbot.backup label"),
other rungs pass, **level=5** (was L2 @ #205). Card PNG shows backup/restore "⊘ INTENTIONAL
SKIP" + reason, level 5 of 5. A formerly-capped non-backup-capable recipe now climbs.
5. **Drone !testme path ×3, GENUINE (not manual API).** ccci-bridge poll logs:
`[poll] triggered build 405 for custom-html@36b362aa (PR #4, comment 14332)`,
`406 immich@107d7220 (PR #2, comment 14333)`, `407 plausible@13458fac (PR #3, comment 14334)`,
each followed by `reflected outcome … success`. Build params confirm RECIPE/PR/REF match the
real PR heads. ≥2 required; 3 delivered, all on real PRs showing the lint rung.
6. **Canaries at re-derived designed level + backup-fail still blocks.** 415 (bkp-bad) / 416
(rst-bad): drone build status=**failure** (red), results.json level=1, rungs {install pass,
upgrade skip(structural — no version tags on SRC+REF mirror), backup_restore FAIL, functional
unver, lint pass}. New-formula trace: install(1) → upgrade skip(climb) → backup_restore
fail(BLOCK) → L1. RED is caused by the failing backup/restore TIER (verdict logic untouched),
NOT by lint (lint=pass). Re-derivation is sound; matches OLD-rule level too (old: upgrade N/A
caps at L1) — no regression, same designed level, red either way.
7. **Unverified-blocks (mission example #3), synthesized.** host run
`/var/lib/cc-ci-runs/lvl5-unver-demo/results.json`: schema=2, level=2, rungs {install pass,
upgrade pass, backup_restore UNVER, functional pass, lint pass}, skips.unintentional=
[backup_restore]. backup unver blocks at L2 even though functional+lint pass above it. ✓
8. **Durations not inflated.** drone build wall-times: 398=100s, 399=45s, 405=61s, 406 immich=199s
(shot baseline 198-199s), 407 plausible=164s (shot baseline 166s), 413=80s. lint adds ~0.7s;
the two cross-phase baselines are flat (407 slightly faster). No duration regression.
9. **Old artifacts render, no relabel.** /runs/370 (schema=1, level=4, level_cap_reason present)
serves 200 (results.json + summary.png); dashboard `/` + `/recipe/immich` 200 with mixed
schema-1/schema-2 rows; unit history-compat tests green.
10. **lint.txt served.** /runs/398/lint.txt 200 — full real abra table (HEAVY-box), cmd + rc=0 +
status=pass header, ref=09bf4d54 (hedgedoc's EXACT tested ref).
11. **Badges number+colour only.** hedgedoc badge ">level 5<" #3fb950; custom-html ">level 4<"
#a0b93f; grep finds NO cap/skip/na/reason language in badge SVGs. Matches operator spec.
12. **P3 matrix 19/19 lint PASS** (BACKLOG-lvl5.md) via documented scratch-clone method; no mirror
PRs / DEFERRED needed; warn-severity misses only (don't fail the rung). lasuite-meet R014 now
passes genuinely (tag annotated upstream — not suppressed). **Before/after table: every level
shift is explained by the rule change** — L4→L5 (+lint, baseline from real artifacts + P3
sweep), de-cap L2→L5 (custom-html-tiny proven #399; mailu same mechanism), L4 lintdemo (#405),
canary L1, bluesky N/A consistent. **No unexplained shift / no downward regression.** "Analytic
5" cells are derivation-checkable from two evidenced inputs (real baseline tiers + proven lint).
13. **No secret leak.** Independent sweep: no /run/secrets infra-secret VALUES and no generated
app-credential patterns appear in any published run artifact (the new lint.txt surface incl.).
results.json flags no_secret_leak=true + clean_teardown=true across runs.
**§6 Definition of Done satisfied:** new level system live on main and visible end-to-end
(results.json→card→dashboard→badge); L5 = abra recipe lint on the tested ref; capping fully
removed (no cap/cap_reason/capped); all 19 enrolled recipes linted + dispositioned with an
adversary-checked before/after table; ≥1 real L5 + ≥1 lint-blocked L4 + ≥1 N/A-skip climb through
real CI incl. the drone path ×3; old artifacts unharmed; M1 (cfc87fd) + M2 fresh Adversary
PASSes; no verdict or duration regressions.
**No VETO. Builder is cleared to write `## DONE` to STATUS-lvl5.md.**
Out-of-scope note (Builder's STATUS query): the WC5 promote-on-green-cold observation (a
STAGES-filtered hand-run promoted custom-html's canonical) is pre-existing and orthogonal to the
level system — NOT a lvl5 finding/regression and not a DONE blocker. If the Builder wants it
tracked, DEFERRED.md/IDEAS.md is the right home; I'm not filing it as an [adversary] finding.

120
STATUS-bsky.md Normal file
View File

@ -0,0 +1,120 @@
# STATUS — phase bsky (fix bluesky-pds recipe + screenshot)
Phase SSOT: /srv/cc-ci/cc-ci-plan/plan-phase-bsky-fix.md
Gate: M1 — CLAIMED, awaiting Adversary.
## M1 claim — root cause + green fix PR + screenshot (2026-06-11T12:05Z)
### WHAT
1. Root cause proven with evidence (below).
2. Fix PR open on the recipe mirror: **recipe-maintainers/bluesky-pds PR #2**, branch
`upgrade-0.3.0+v0.4.219`, head `f7b6c8df` — 2-line compose.yml diff (image
`ghcr.io/bluesky-social/pds:0.4``0.4.219`; version label `0.2.0+v0.4`
`0.3.0+v0.4.219`). UNMERGED (operator merges).
3. `!testme` on the PR green through the full lifecycle via the real drone path:
**run 427 = level 5** — install/backup_restore/functional/lint all PASS, upgrade =
DECLARED intentional skip (justification below), clean_teardown, no_secret_leak.
4. Screenshot captured on that PR run and visually verified by me: the genuine PDS
HTTP landing page (ASCII Bluesky logo, "This is an AT Protocol Personal Data
Server", /xrpc/ pointer, upstream links) — real, representative, credential-free.
No SCREENSHOT hook needed.
### Root cause
The recipe pins MOVING tag `ghcr.io/bluesky-social/pds:0.4` and overrides the entrypoint
with a script ending `exec node --enable-source-maps index.js` (relative to WORKDIR /app).
Upstream now publishes main-branch builds to `:0.4` (== `latest`, manifest
`sha256:871194d2…`, created 2026-05-30): `@atproto/pds` **0.5.1**, Node v24.15.0, service
restructured to `/app/index.ts` (CMD `node --enable-source-maps index.ts`; **no
index.js**) → crash-loop `Cannot find module '/app/index.js'`. Exact tag `0.4.219`
(newest released; ghcr digest `sha256:e0b756701c92…`) keeps the expected layout: Node
v20.20.2, `/app/index.js`, dumb-init, CMD identical to the recipe's exec line.
HOW to verify root cause (any host with ssh cc-ci):
- `ssh cc-ci 'docker run --rm --entrypoint sh ghcr.io/bluesky-social/pds:0.4 -c "node --version; ls /app; grep @atproto/pds /app/package.json"'`
→ EXPECTED v24.15.0; index.ts, NO index.js; `"@atproto/pds": "0.5.1"`
- `ssh cc-ci 'docker run --rm --entrypoint sh ghcr.io/bluesky-social/pds:0.4.219 -c "node --version; ls /app; grep @atproto/pds /app/package.json"'`
→ EXPECTED v20.20.2; index.js present; `"@atproto/pds": "0.4.219"`
- Upstream: Dockerfile@main = node:24.15-alpine3.23 + CMD index.ts;
Dockerfile@v0.4.219 = node:20.20-alpine3.23 + CMD index.js. Registry doc:
cc-ci-plan/upstream/bluesky-pds.md (plan repo f395247).
### Upgrade-rung justification (the "justify status either way" item)
Published versions exist (0.1.1+v0.4, 0.2.0+v0.4) but BOTH pin the republished `:0.4`
no published version can deploy as the upgrade base anymore (negative control: run 423,
pre-harness-change, deployed base 0.1.1+v0.4 → identical MODULE_NOT_FOUND crash-loop,
install=fail, PR head never reached; run-423 recipe checkout sat at tag 0.1.1+v0.4).
Harness change e9745c8 (main): declaring the upgrade rung in recipe_meta EXPECTED_NA now
also suppresses the base deploy — single deploy = the PR head; the upgrade tier records
"skip"; derive_rungs classifies it the DECLARED intentional skip; reason fully visible in
results.json `skips.intentional` and on the card. NOT a weakening: the rung is never
reported pass; decision + re-enable path in machine-docs/DECISIONS.md (re-enable =
UPGRADE_BASE_VERSION="0.3.0+v0.4.219" once merged+published).
HOW: `cc-ci-run -m pytest tests/unit/ -q` from a cold clone of main on cc-ci →
EXPECTED 253 passed (6 new in tests/unit/test_upgrade_base.py);
`nix develop .#lint -c bash scripts/lint.sh` → EXPECTED `lint: PASS`.
### Green-run evidence (run 427, drone path)
- Trigger: PR #2 comment 14342 (`!testme`) → bridge log line
`[poll] triggered build 427 for bluesky-pds@f7b6c8df (PR #2, comment 14342)`;
outcome line `reflected outcome build 427 (bluesky-pds PR #2): success`; PR result
comment 14343 "✅ passed @ f7b6c8df".
- HOW: `ssh cc-ci 'cat /var/lib/cc-ci-runs/427/results.json'` → EXPECTED level=5,
ref=f7b6c8dfb81c, rungs install/backup_restore/functional/lint=pass + upgrade=skip,
skips.intentional.upgrade=<declared reason>, flags clean_teardown+no_secret_leak true.
- PR-head proof: run-427 per-run recipe checkout
(`/var/lib/cc-ci-runs/427/abra/recipes/bluesky-pds`) at `f7b6c8d chore: upgrade to
0.3.0+v0.4.219`, compose.yml line 6 image=…:0.4.219.
- Visuals: https://ci.commoninternet.net/runs/427/summary.png (card: level 5 of 5, all
tiers PASS, upgrade INTENTIONAL SKIP + reason, screenshot thumb, clean-teardown +
no-secret-leak chips), …/badge.svg ("cc-ci: level 5", green),
…/screenshot.png (the PDS landing page described above).
### WHERE
- cc-ci main @ 72b3d6c (harness change e9745c8; journal/decisions 72b3d6c).
- Mirror PR #2: https://git.autonomic.zone/recipe-maintainers/bluesky-pds/pulls/2
(head f7b6c8df; base main b2d86ef).
- Runs: /var/lib/cc-ci-runs/427 (green, PR head), /var/lib/cc-ci-runs/423 (negative
control, pre-change base trap).
- Upstream registry: cc-ci-plan/upstream/bluesky-pds.md @ plan-repo f395247.
## Operator summary
**What was wrong.** bluesky-pds could not deploy at all: the app crash-looped
`Cannot find module '/app/index.js'`. The recipe pins the MOVING image tag
`ghcr.io/bluesky-social/pds:0.4`, and upstream now republishes that tag with main-branch
builds (currently @atproto/pds 0.5.1 on Node 24, where the service entrypoint moved to
`/app/index.ts``index.js` no longer exists). The recipe's entrypoint override
(`exec node --enable-source-maps index.js`) can no longer resolve. This also silently
broke BOTH previously published recipe versions (0.1.1+v0.4, 0.2.0+v0.4 — same moving
pin), so no historical version can deploy anymore either.
**What the PR changes.** https://git.autonomic.zone/recipe-maintainers/bluesky-pds/pulls/2
(branch `upgrade-0.3.0+v0.4.219`, head f7b6c8df), a 2-line compose.yml diff: pin the exact
released tag `0.4.219` (newest released; classic Node 20 / index.js layout the recipe's
entrypoint expects) and bump the version label to `0.3.0+v0.4.219`. Why not 0.5.1: it has
no release tag (only the moving :0.4/latest + sha- tags from main) and needs an entrypoint
migration; do that as a proper upgrade when upstream cuts a 0.5.x release tag (notes in
cc-ci-plan/upstream/bluesky-pds.md). Proven at PR head via real drone CI: run 427 =
**level 5** (install, backup/restore, functional, lint PASS; screenshot = real PDS landing
page). The upgrade rung is a DECLARED intentional skip — there is no deployable published
base to upgrade FROM (see above); declaration + reason in tests/bluesky-pds/recipe_meta.py.
**What to do post-merge.**
1. Merge PR #2 (your call, as with immich PR#2 / plausible PR#3 — all left open).
2. Publish the version per recipe convention (annotated tag `0.3.0+v0.4.219` /
`abra recipe release`) so `abra recipe versions` lists a deployable version again.
3. After the tag is published: in cc-ci `tests/bluesky-pds/recipe_meta.py`, DROP the
`EXPECTED_NA["upgrade"]` declaration and set
`UPGRADE_BASE_VERSION = "0.3.0+v0.4.219"` — the upgrade rung then re-activates from
the first deployable base (the older broken tags must never be auto-picked as base).
4. Canonical/warm: nothing to reseed — bluesky-pds has no canonical
(/var/lib/ci-warm has no entry); the normal promote-on-green flow mints one on the
first green run post-merge.
5. Never re-pin this recipe to `:0.4`/`latest` — upstream demonstrably republishes the
minor tag (registry notes: cc-ci-plan/upstream/bluesky-pds.md).

View File

@ -1,6 +1,71 @@
# STATUS — Phase lvl5 (L5 lint rung + de-cap)
Phase: lvl5 — OPEN (bootstrapped 2026-06-11)
Gate: none claimed yet
In flight: P1 — level.py new semantics + lint executor design (abra lint behavior probe on CI host first)
Blockers: none
## DONE
Phase complete 2026-06-11: M1 PASS (cfc87fd) + M2 PASS (13cad1f), both <24h, no VETO.
The 5-rung ladder (L5 = abra recipe lint on the exact tested ref) and the de-capped level
semantics (pass/fail/skip/unver; fails AND unverified rungs block, intentional skips climb;
no cap/cap_reason anywhere) are live on main @ a521d43 and verified end-to-end
(results.json schema 2 card dashboard badge PR comment, drone path included).
Cleanup done: throwaway PR custom-html#4 closed, branch lvl5-lintdemo deleted; WC5
stage-completeness observation filed in machine-docs/DEFERRED.md.
## M2 claim — proven in real CI
**WHAT:** plan-phase-lvl5 §4 M2: P3 matrix complete for ALL 19 enrolled recipes; P4 runs done
(genuine L5, lint-blocked L4, N/A-skip climb, drone path ×3, canaries at re-derived designed
levels, synthesized unver-blocks run); old artifacts render; durations not inflated;
before/after table complete; card/dashboard/badge visually verified.
**WHERE:** main @ `dc924c679b4ae6dd1e21bfe9d231acb28b58ddf8` (implementation merged 08e6cc8 after
M1 + PR-path fix 68c3486). Evidence runs (all artifacts at
`https://ci.commoninternet.net/runs/<n>/{results.json,summary.png,badge.svg,lint.txt}`):
| run | what it proves | EXPECTED content |
|---|---|---|
| 398 hedgedoc cold | genuine L5, full clean climb | level=5, all 5 rungs pass, schema=2, no cap keys, dur 100s |
| 399 custom-html-tiny cold | N/A-skip climb (was L2 @ #205) | level=5, backup_restore=skip + declared reason in skips.intentional, dur 45s |
| 405 custom-html PR4 (!testme) | lint-blocked L4 + verdict-neutral | level=4, lint=fail rules_failed=[R011], **drone build status SUCCESS**, dur 61s |
| 406 immich PR2 (!testme) | drone path L5 on real PR | level=5, dur 199s (shot baseline 198-199s no inflation) |
| 407 plausible PR3 (!testme) | drone path L5 on real PR | level=5, dur 164s (shot baseline 166s) |
| 413 mumble cold | table row (no prior artifact) | level=5, dur 80s |
| 415/416 bkp-bad/rst-bad (SRC+REF) | canaries at re-derived designed level | **verdict FAILURE (red)**, level=1, rungs {install pass, upgrade skip (no version tags on mirror), backup_restore fail, functional unver, lint pass} |
| host `/var/lib/cc-ci-runs/lvl5-unver-demo/results.json` | synthesized unver-blocks (mission ex. #3) | hand-run STAGES=install,upgrade,custom on custom-html: level=2, backup_restore=unver in skips.unintentional, functional+lint pass above it |
**HOW to verify (cold):**
1. Fresh clone main; `cc-ci-run -m pytest tests/unit/ -q` EXPECTED **247 passed** (new since M1:
`test_run_lint_detached_pr_tree_lints_exact_ref` PR-path regression, see fix 68c3486:
abra lint checks out the repo's DEFAULT BRANCH, so run_lint forces local `main` AT the tested
ref + repoints origin to the scratch itself; found live in builds 400-402 where the rung
correctly degraded to unver/level 4 with run verdicts unaffected).
`nix develop .#lint --command bash scripts/lint.sh` PASS.
2. Fetch each run's results.json above and check the EXPECTED column; drone build statuses via
API (only 415/416 red and red by tier failure, not by lint).
3. Visuals: Read `summary.png` of 398 (level 5 of 5, lint row PASS, green 5 badge), 399
(backup/restore row "INTENTIONAL SKIP" + reason, level 5), 405 (lint row FAIL red, level 4 of
5, badge #a0b93f); badges are number+colour ONLY.
4. Old artifacts: `/runs/370/{results.json,summary.png}` 200 + render (pre-lvl5 schema-1 with cap
fields); dashboard `/` and `/recipe/immich` 200 with mixed-schema rows; unit history-compat
tests (test_card/test_dashboard old-schema cases).
5. lint.txt served: `/runs/398/lint.txt` 200 (full abra table; rc/status header).
6. P3 matrix + §2.9 before/after table: BACKLOG-lvl5.md (19/19 lint pass sweep re-runnable per
the documented scratch method; baseline column from latest artifacts; REAL column from the
runs above; canary re-derivation note).
7. Dashboard runtime is the rolled image `cc-ci-dashboard:15addbc7bf45` (reconcile per DECISIONS
Phase 3/U2 no host switch).
**Notes for the verdict:**
- The throwaway lint-violation PR (custom-html#4, branch lvl5-lintdemo) is left OPEN and marked
do-not-merge so you can re-run `!testme` independently; Builder will close branch+PR after M2.
- Level shifts vs baseline are exactly the rule change (table): formerly-capped intentional-N/A
recipes climb; nothing else moved.
- Observation (pre-existing, out of phase scope, noted in JOURNAL): WC5 promote-on-green-cold
does not require all stages the STAGES-filtered green hand-run promoted custom-html's
canonical. Filed as a JOURNAL note; flag if you want it as a finding.
---
## (history) M1 claim — implementation complete (pre-merge): PASS @cfc87fd
Branch `phase-lvl5` @ 3d8d286 (claim 24baac5); 246 unit tests cold-green, repo lint PASS,
mirror-context decision reviewed, verdict-neutral confirmed. Merged to main 08e6cc8.

View File

@ -116,7 +116,7 @@ _This table is GENERATED from the `runner/harness/meta.py` KEYS registry by `scr
| `DEPLOY_TIMEOUT` | `int` | `600` | Max seconds to wait for swarm convergence per deploy. |
| `HTTP_TIMEOUT` | `int` | `300` | Max seconds to wait for HTTP health after convergence. |
| `BACKUP_CAPABLE` | `bool` | `None` | Override the backup-tier capability auto-detect (compose `backupbot.backup` labels). `False` forces an intentional skip of the backup/restore rung; `True` forces the tier on; unset = auto-detect. |
| `EXPECTED_NA` | `dict` | `None` | Declare a non-run rung an INTENTIONAL skip: `{rung: reason}` — the level climbs past it; an undeclared non-run rung is *unverified* and blocks the level above it (classification table: machine-docs/DECISIONS.md phase lvl5). Never overrides an exercised pass/fail; the `lint` rung has no escape hatch. |
| `EXPECTED_NA` | `dict` | `None` | Declare a non-run rung an INTENTIONAL skip: `{rung: reason}` — the level climbs past it; an undeclared non-run rung is *unverified* and blocks the level above it (classification table: machine-docs/DECISIONS.md phase lvl5). Never overrides an exercised pass/fail; the `lint` rung has no escape hatch. Declaring `upgrade` also suppresses the upgrade-tier BASE deploy — the single deploy is the PR head itself — for recipes whose published versions exist but are genuinely undeployable (phase bsky). |
| `READY_PROBE` | `hook` | `None` | Callable `(ctx) -> [probe, ...]` returning extra readiness probes, run after install AND after upgrade: HTTP `{host, path, ok}` or TCP `{tcp_host, tcp_port, stable}`. |
| `UPGRADE_BASE_VERSION` | `str` | `None` | Exact published tag overriding the upgrade tier's base (default: `recipe_versions[-2]`). |
| `BACKUP_VERIFY` | `hook` | `None` | Callable `(ctx) -> bool` post-backup data-capture check; `False` re-runs the backup (truncated-dump race guard), retried up to 3 attempts. |

View File

@ -1353,3 +1353,33 @@ recipe"); pass iff the table rendered clean; anything else unver + loud log. Har
(observed ~0.7s); executor runs before the tiers (tree at tested ref), double-wrapped, R7
verdict-neutral. Full output → run artifact `lint.txt` (dashboard-served); status + failing
rule ids → results.json `lint`.
**bluesky-pds re-pin decision (phase bsky, 2026-06-11).** The recipe pinned the moving tag
`ghcr.io/bluesky-social/pds:0.4`, which upstream now republishes with main-branch builds
(currently @atproto/pds 0.5.1, Node 24, `/app/index.ts` — no `index.js`), breaking the
recipe's entrypoint override (`exec node --enable-source-maps index.js`). Fix: pin the
newest RELEASED exact tag `0.4.219` (Node 20.20, `/app/index.js`, CMD identical to the
recipe's exec line — entrypoint stays valid unchanged) and bump the version label
`0.2.0+v0.4` → `0.3.0+v0.4.219` (minor bump for an upstream pin change, immich-PR#2
precedent). REJECTED: tracking 0.5.1 (only exists as moving/sha- tags built from main —
no release tag; would also require entrypoint `index.ts` migration against an unreleased
version); digest-suffix pinning (abra survey/upgrade tooling chokes on tag@digest — see
immich standing note). When upstream cuts real 0.5.x release tags, upgrade properly
(entrypoint will then need the index.ts/Node-24 migration — recorded in
cc-ci-plan/upstream/bluesky-pds.md). Never re-pin to `:0.4`/`latest`/minor tags.
**EXPECTED_NA["upgrade"] suppresses the upgrade-tier base deploy (phase bsky, 2026-06-11).**
The deploy-once design deploys the upgrade BASE (previous published version) and only the
upgrade tier chaos-redeploys the PR head — so a recipe whose published versions ALL became
undeployable (bluesky-pds: every tag pins moving `ghcr.io/bluesky-social/pds:0.4`, which
upstream republished with incompatible main builds) fails INSTALL at the base before the PR
head is ever exercised, and no UPGRADE_BASE_VERSION value can help (it must be a published
tag — they're all broken). Decision: declaring the upgrade rung in EXPECTED_NA (the existing
intentional-skip mechanism) now ALSO makes upgrade_base() return None → the single deploy is
the PR head itself; the upgrade tier records "skip"; derive_rungs classifies it as the
DECLARED intentional skip with the recipe's reason (results.json skips.intentional). NOT a
gate weakening: the rung is never reported pass, the skip + reason are fully visible, and the
declaration is evidence-backed in the recipe_meta comment + upstream registry; it is the only
way to exercise a PR at all for a recipe in this state. Re-enable path documented per-recipe
(bluesky: drop EXPECTED_NA + set UPGRADE_BASE_VERSION="0.3.0+v0.4.219" once merged+published).
Locked by tests/unit/test_upgrade_base.py.

View File

@ -118,6 +118,7 @@ before the build is called done) — but does **not** force closure.
- **Linked IDEA:** —
### 2026-05-28 — uptime-kuma create-a-monitor (§4.3 prescribed)
- [x] **RE-ENTERED @2026-06-11:** operator approved — executing as phase `kuma` (cc-ci-plan/plan-phase-kuma-monitor.md).
- [ ] **What:** Add a test that completes uptime-kuma's first-run setup wizard via Socket.IO,
logs in to obtain a JWT, creates a monitor (`monitor add` Socket.IO emit), and asserts the
monitor appears in the listed-monitors response.
@ -210,6 +211,7 @@ before the build is called done) — but does **not** force closure.
(none yet — append `### YYYY-MM-DD — <slug> CLOSED (commit/PR)` here when re-entered.)
### 2026-05-28 — plausible (Q4.7) recipe enrollment
- [x] **CLOSED @2026-06-11 (operator housekeeping):** overtaken — plausible is enrolled and running in CI (§4.3 floor `71af595`); the full-lifecycle remainder is the Q4.7b entry below (recipe PR#3 green, operator merge pending).
- [ ] **What:** Enroll plausible in cc-ci with parity health_check + ≥2 specific tests (per
plan §4.3: "track a test event, query it back"). `tests/plausible/recipe_meta.py` +
`tests/plausible/functional/test_health_check.py` are drafted (commit pending) but the
@ -237,6 +239,7 @@ before the build is called done) — but does **not** force closure.
Defensible defer; lift when the operator wants the deeper coverage OR Phase-4 reviews.
### 2026-05-29 — immich recipe needs a pg_dump backup hook for reliable DB restore (P4)
- [x] **CLOSED @2026-06-11:** cc-ci-authored immich recipe PR#2 (pg_dump hook) verified green; operator confirmed 2026-06-11 — merge pending, no further loop work.
- [ ] **What:** immich's upstream recipe backs up the LIVE postgres data VOLUME via restic
(`backupbot.backup=true` on `database`, no pg_dump hook), so a DB row does NOT survive
`abra app restore` (diagnosed: seed→backup→drop→restore→row absent; app healthy). Real
@ -256,6 +259,7 @@ before the build is called done) — but does **not** force closure.
- **Linked IDEA:** —
### 2026-05-29 — discourse: upstream recipe pins removed bitnami images (undeployable)
- [x] **CLOSED @2026-06-11 (operator housekeeping):** superseded — discourse is enrolled and runs the full lifecycle in CI (L4 baseline run 184, 2026-06-05); the bitnami-pin blocker no longer applies.
- [ ] **What:** discourse (Q4.6) cannot be enrolled/tested because the recipe pins
`image: bitnami/discourse:<tag>` (app + sidekiq) and **Docker Hub no longer serves any
`bitnami/discourse:*` tag** (bitnami's 2024/2025 legacy migration). Proven on cc-ci:
@ -282,6 +286,7 @@ before the build is called done) — but does **not** force closure.
- **Linked IDEA / BACKLOG:** Q4.6.
### 2026-05-29 — mailu: no backup config (P4 N/A) — recipe-PR to add backupbot
- [x] **RE-ENTERED @2026-06-11:** operator approved the backupbot recipe-PR route — executing as phase `mailu` (cc-ci-plan/plan-phase-mailu-backup.md).
- [ ] **What:** mailu (Q4.9) ships **no `backupbot.backup` label** on any service, so cc-ci's
backup/restore tiers cleanly SKIP (`backup_capable=False`) — P4 (backup data-integrity) is N/A
for mailu as published (no backup mechanism to exercise). Durable fix = a recipe-PR adding
@ -296,6 +301,7 @@ before the build is called done) — but does **not** force closure.
- **Linked IDEA / BACKLOG:** Q4.9.
### 2026-05-29 — drone (Q4.10) blocked on host /etc/timezone deploy (gitea SCM dep) + scoped integration
- [x] **RE-ENTERED @2026-06-11:** operator approved — executing as phase `drone` (cc-ci-plan/plan-phase-drone-enroll.md); P0 host /etc/timezone deploy is orchestrator-owned.
- [ ] **What:** drone (Q4.10, LAST §5 recipe) cannot be enrolled until two things land:
(1) **HOST FIX — operator-deploy needed:** drone is a CI server that REQUIRES a git-provider SCM
to boot; the only viable dep is **gitea**, which the recipe binds `/etc/timezone:ro` from the
@ -322,6 +328,7 @@ before the build is called done) — but does **not** force closure.
- **Linked IDEA / BACKLOG:** Q4.10; JOURNAL-2 f86a58a; commit 3bde76f.
### 2026-05-30 — plausible Q4.7 full (recipe-PR Q4.7b: fix ClickHouse entrypoint wget restart-storm)
- [x] **CLOSED @2026-06-11:** recipe PR#3 (ClickHouse entrypoint + backup fixes) verified GREEN at PR head; operator confirmed 2026-06-11 — merge pending. Post-merge follow-up: full lifecycle on main to formally claim Q4.7.
- [ ] **What:** Fix the recipe `entrypoint.clickhouse.sh` so ClickHouse boots reliably, then run
plausible's FULL lifecycle (`install,upgrade,backup,restore,custom`) green + claim Q4.7. Suite
authored (`tests/plausible/` ops + test_backup/restore/upgrade + event-roundtrips); §4.3 floor
@ -335,8 +342,8 @@ before the build is called done) — but does **not** force closure.
- **Re-entry trigger:** Builder authors recipe-PR Q4.7b (cache tarball on a volume / wget
retry+backoff / drop `2>/dev/null` / `set +e` w/ fallback), then runs plausible-full green + claims.
- **Linked:** REVIEW-2 `e850281` (root-cause + DENY), `71af595` (§4.3 floor); DECISIONS 2026-05-30.
- discourse upgrade-HC1 @7ae7b0f stamps prev-base tag commit (eb96de94+U) on BOTH old+new harness since ~06-10 (baseline 184 was L4 on 06-05); harness-neutral (rcust exonerated, M2-closed) but abra stamp-resolution mechanism UNATTRIBUTED — worth a standalone dig outside rcust. Evidence: /var/lib/cc-ci-runs/{m2p-discourse,ab-discourse-7ae7b0f-oldmain}, JOURNAL-rcust 2026-06-11.
- bluesky-pds: UPSTREAM IMAGE BREAKAGE (non-rcust, M2-justified exclusion from baseline match).
- [RE-ENTERED @2026-06-11 → phase `dstamp` (cc-ci-plan/plan-phase-dstamp-discourse-drift.md)] discourse upgrade-HC1 @7ae7b0f stamps prev-base tag commit (eb96de94+U) on BOTH old+new harness since ~06-10 (baseline 184 was L4 on 06-05); harness-neutral (rcust exonerated, M2-closed) but abra stamp-resolution mechanism UNATTRIBUTED — worth a standalone dig outside rcust. Evidence: /var/lib/cc-ci-runs/{m2p-discourse,ab-discourse-7ae7b0f-oldmain}, JOURNAL-rcust 2026-06-11.
- [RE-ENTERED @2026-06-11 → phase `bsky`] ✅ **RESOLVED @2026-06-11 (phase bsky, Builder):** root cause = upstream republishes the MOVING tag `:0.4` with main-branch builds (now @atproto/pds 0.5.1, Node 24, `/app/index.ts` — no `index.js`), breaking the recipe's entrypoint override. Fix PR open (operator merges): **recipe-maintainers/bluesky-pds PR #2** (`upgrade-0.3.0+v0.4.219`, head f7b6c8df — exact-pin `0.4.219` + version-label bump). Proven green at PR head via real drone CI: run 427 **level 5** (install/backup_restore/functional/lint PASS; upgrade = declared intentional skip — no deployable published base, both old tags pin the republished `:0.4`; negative control run 423). Screenshot real (PDS landing page). The shot-phase deploy-gated N/A is lifted on the PR runs. Upstream registry: cc-ci-plan/upstream/bluesky-pds.md; decisions: DECISIONS.md 2026-06-11 (pin choice + EXPECTED_NA-upgrade base suppression). Both the re-pin follow-up AND the rcust M2 exclusion note are hereby closed with these pointers. Original entry follows: bluesky-pds: UPSTREAM IMAGE BREAKAGE (non-rcust, M2-justified exclusion from baseline match).
The app container crash-loops `Error: Cannot find module '/app/index.js'` (MODULE_NOT_FOUND,
Node v24.15.0) under the recipe's pinned tag on EVERY current run — new main @ mirror head
(m2r-bluesky-pds), new main serial re-run (m2rr-bluesky-pds), AND old pre-rcust main @ old
@ -360,3 +367,13 @@ before the build is called done) — but does **not** force closure.
Evidence: /tmp/mumble-probe{2,3,4}.out + /tmp/mumble-orch{4,5}.log on cc-ci (90s DOM/console/
network observation; websockify reachable, /ws & /websocket 404 from websockify itself);
/var/lib/cc-ci-runs/shot-proof-mumble/screenshot.png (L4 run, loader frame).
## WC5 promote-on-green-cold ignores stage completeness (filed 2026-06-11, Builder, phase lvl5)
Observed during the lvl5 unver-blocks proof: a GREEN hand-run with `STAGES=install,upgrade,custom`
(backup/restore excluded) on latest still advanced custom-html's warm canonical —
`should_promote_canonical` checks green+cold+latest but not that ALL stages ran. Pre-existing
behavior (not introduced or worsened by lvl5; Adversary concurs it is not a finding). Only
reachable via the operator/dev STAGES escape — production drone runs always run all stages.
**Needed from operator:** decide whether promote should additionally require the full stage set
(one-line guard in `should_promote_canonical`), or whether dev hand-runs promoting is acceptable.

View File

@ -128,14 +128,35 @@ def run_lint(recipe: str, ref: str | None, out_dir: str | None) -> dict:
text=True,
timeout=LINT_TIMEOUT,
)
if ref:
subprocess.run(
["git", "-C", clone, "checkout", "-f", "--quiet", ref],
check=True,
capture_output=True,
text=True,
timeout=LINT_TIMEOUT,
)
# abra lint SELECTS AND CHECKS OUT THE REPO'S DEFAULT BRANCH before linting (observed
# live, build 400-402: a clone of a detached-HEAD per-run tree has no local branch →
# FATA "failed to select default branch"; and if a default branch existed at some OTHER
# commit, abra would silently lint THAT, not the tested ref). So: force a local `main`
# AT exactly the tested ref and make it the default everywhere abra could look —
# HEAD, and origin (repointed to the scratch itself, which also turns abra's tag
# force-fetch into an offline no-op; the run's true tags were already cloned in).
subprocess.run(
["git", "-C", clone, "checkout", "-f", "--quiet", "-B", "main"]
+ ([ref] if ref else []),
check=True,
capture_output=True,
text=True,
timeout=LINT_TIMEOUT,
)
subprocess.run(
["git", "-C", clone, "remote", "set-url", "origin", clone],
check=True,
capture_output=True,
text=True,
timeout=LINT_TIMEOUT,
)
subprocess.run(
["git", "-C", clone, "remote", "set-head", "origin", "main"],
check=False, # cosmetic: helps any origin-HEAD-based default-branch lookup
capture_output=True,
text=True,
timeout=LINT_TIMEOUT,
)
# catalogue: R006 (published catalogue version) reads it; servers: harmless, some abra
# paths stat it. Symlink the live ones (read-only use).
for shared in ("catalogue", "servers"):

View File

@ -76,7 +76,7 @@ KEYS: tuple[Key, ...] = (
"EXPECTED_NA",
"dict",
None,
"Declare a non-run rung an INTENTIONAL skip: `{rung: reason}` — the level climbs past it; an undeclared non-run rung is *unverified* and blocks the level above it (classification table: machine-docs/DECISIONS.md phase lvl5). Never overrides an exercised pass/fail; the `lint` rung has no escape hatch.",
"Declare a non-run rung an INTENTIONAL skip: `{rung: reason}` — the level climbs past it; an undeclared non-run rung is *unverified* and blocks the level above it (classification table: machine-docs/DECISIONS.md phase lvl5). Never overrides an exercised pass/fail; the `lint` rung has no escape hatch. Declaring `upgrade` also suppresses the upgrade-tier BASE deploy — the single deploy is the PR head itself — for recipes whose published versions exist but are genuinely undeployable (phase bsky).",
),
Key(
"READY_PROBE",

View File

@ -88,6 +88,38 @@ def sso_dep_unverified(declared, deps_ready: bool, requires_deps_skipped: int) -
return bool(declared) and not deps_ready and requires_deps_skipped > 0
def upgrade_base(stages, meta, recipe: str) -> str | None:
"""Deploy-once base version decision (pure given meta + the published-version lookup):
previous published version when the upgrade tier will run and one exists (so upgrade goes
previous→target in place), else None (the caller falls back to the target / PR head).
(DECISIONS.)
A recipe may override the base via recipe_meta UPGRADE_BASE_VERSION when the harness default
(recipe_versions[-2]) is NOT the PR's true predecessor — e.g. a PR that adds a version ABOVE the
newest published tag, where the correct base is [-1] (the newest published), not [-2]. The
override must be an exact published version tag (deployed as a pinned base). (Adversary §7.1.)
A recipe that declares the upgrade rung in EXPECTED_NA gets NO base: published versions may
exist yet be genuinely undeployable — e.g. bluesky-pds, where every published tag pins the
moving image tag `:0.4` that upstream republished with incompatible main builds, so no
published version can come up as an upgrade base (phase bsky, DECISIONS). Deploying one would
fail the INSTALL tier before the PR-head code is ever exercised. With no base, the single
deploy is the PR head itself and the upgrade tier records "skip", which derive_rungs
classifies as the DECLARED intentional skip (reason from EXPECTED_NA — visible in
results.json `skips.intentional`, never reported as a pass)."""
if "upgrade" not in stages:
return None
if "upgrade" in (meta.EXPECTED_NA or {}):
print(
"== upgrade tier: declared EXPECTED_NA['upgrade'] — no upgrade base will be "
f"deployed; the single deploy is the target/PR head. Reason: "
f"{(meta.EXPECTED_NA or {}).get('upgrade')}",
flush=True,
)
return None
return meta.UPGRADE_BASE_VERSION or lifecycle.previous_version(recipe)
def _truthy(v: str | None) -> bool:
return str(v or "").strip().lower() in ("1", "true", "yes", "on")
@ -905,16 +937,7 @@ def main() -> int:
domain = naming.app_domain(recipe, os.environ.get("PR", "0"), ref)
# Deploy-once base version: previous published version when the upgrade tier will run and one
# exists (so upgrade goes previous→target in place), else the target (current/$REF). (DECISIONS.)
# A recipe may override the base via recipe_meta UPGRADE_BASE_VERSION when the harness default
# (recipe_versions[-2]) is NOT the PR's true predecessor — e.g. a PR that adds a version ABOVE the
# newest published tag, where the correct base is [-1] (the newest published), not [-2]. The
# override must be an exact published version tag (deployed as a pinned base). (Adversary §7.1.)
want_upgrade = "upgrade" in stages
prev = (
(meta.UPGRADE_BASE_VERSION or lifecycle.previous_version(recipe)) if want_upgrade else None
)
prev = upgrade_base(stages, meta, recipe)
base = prev or target
backup_cap = generic.backup_capable(recipe, meta)
hook = discovery.install_steps(recipe, repo_local)
@ -1091,7 +1114,7 @@ def main() -> int:
junit_dir=junit_dir,
)
if prev
else "skip" # only one published version → nothing to upgrade from
else "skip" # no upgrade base: single published version, or declared EXPECTED_NA
)
# ---- BACKUP + RESTORE tiers (backup-capable only; else clean N/A) ----
if "backup" in stages:
@ -1274,7 +1297,7 @@ def main() -> int:
records=records,
results=results,
backup_capable=backup_cap,
has_upgrade_target=prev is not None, # structural: a previous published version exists
has_upgrade_target=prev is not None, # structural: a deployable upgrade base exists
lint=lint_result, # L5 rung (phase lvl5)
clean_teardown=clean_teardown,
no_secret_leak=True, # narrowed below by an actual scan of the serialised artifact

View File

@ -6,3 +6,17 @@ HEALTH_PATH = "/xrpc/_health" # PDS health endpoint; returns {"version": ...} o
HEALTH_OK = (200,)
DEPLOY_TIMEOUT = 600
HTTP_TIMEOUT = 600
# UPGRADE rung: published versions exist (0.1.1+v0.4, 0.2.0+v0.4) but BOTH pin the moving image
# tag ghcr.io/bluesky-social/pds:0.4, which upstream republished with main-branch builds
# (@atproto/pds 0.5.1, Node 24, /app/index.ts — no index.js), so NO published version can deploy
# as an upgrade base anymore: the base crash-loops MODULE_NOT_FOUND before the PR head is ever
# exercised (phase bsky root cause; cc-ci-plan/upstream/bluesky-pds.md). Declared intentional
# until a fixed exact-pinned version (0.3.0+v0.4.219, mirror PR #2) is merged AND published —
# then DROP this and set UPGRADE_BASE_VERSION = "0.3.0+v0.4.219" so the upgrade rung is
# exercised again from the first deployable base.
EXPECTED_NA = {
"upgrade": "no deployable upgrade base: every published version pins the moving tag "
"pds:0.4, which upstream republished with incompatible main builds (index.js removed) — "
"re-enable via UPGRADE_BASE_VERSION once a fixed version is published post-merge",
}

View File

@ -183,6 +183,41 @@ def test_run_lint_missing_recipe_is_unver_not_raise(tmp_path, monkeypatch):
assert (tmp_path / "artifacts" / "lint.txt").exists()
def test_run_lint_detached_pr_tree_lints_exact_ref(tmp_path, monkeypatch):
# PR-path regression (live builds 400-402): the per-run tree sits at a DETACHED HEAD (the PR
# sha), and abra lint selects+checks out the repo's DEFAULT BRANCH before linting. run_lint
# must (a) not FATA on the branchless clone, and (b) make the default branch BE the tested
# ref — never some other commit's content. Source repo: branch main at C1, detached at C2
# (the "PR head", which adds marker-c2). The shim passes only if the linted tree contains
# marker-c2 AND HEAD is a local branch.
repo = _mkrecipe(tmp_path)
(repo / "marker-c2.txt").write_text("pr head\n")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "c2"],
cwd=repo,
check=True,
)
c2 = subprocess.run(
["git", "rev-parse", "HEAD"], cwd=repo, check=True, capture_output=True, text=True
).stdout.strip()
subprocess.run(["git", "branch", "-f", "main", "HEAD^"], cwd=repo, check=True)
subprocess.run(["git", "checkout", "-q", c2], cwd=repo, check=True) # detached, like a PR run
monkeypatch.setenv("ABRA_DIR", str(tmp_path / "abra"))
out = TABLE_OK.replace("\r\n", "\\n")
shim = (
'C="$ABRA_DIR/recipes/fakerec"\n'
'git -C "$C" symbolic-ref HEAD >&2 || { echo "FATA no default branch" >&2; exit 1; }\n'
'[ -f "$C/marker-c2.txt" ] || { echo "FATA wrong ref linted" >&2; exit 1; }\n'
f'printf "{out}"\nexit 0\n'
)
monkeypatch.setenv("PATH", _shim(tmp_path, shim) + os.pathsep + os.environ["PATH"])
res = L.run_lint("fakerec", c2, str(tmp_path / "artifacts"))
assert res == {"status": "pass", "detail": "", "rules_failed": []}
txt = (tmp_path / "artifacts" / "lint.txt").read_text()
assert "refs/heads/main" in txt # HEAD was a local default branch when abra ran
def test_run_lint_abra_blowup_is_unver(tmp_path, monkeypatch):
_mkrecipe(tmp_path)
monkeypatch.setenv("ABRA_DIR", str(tmp_path / "abra"))

View File

@ -0,0 +1,79 @@
"""Unit tests for `run_recipe_ci.upgrade_base` — the deploy-once base-version decision.
Phase bsky: a recipe whose published versions ALL pin a moving image tag that upstream
republished with incompatible builds (bluesky-pds) has no deployable upgrade base — deploying
one fails the INSTALL tier before the PR head is ever exercised. The sanctioned escape hatch is
the EXISTING declared-intentional-skip mechanism: EXPECTED_NA["upgrade"] now also suppresses
the base deploy (single deploy = PR head; the tier records "skip"; derive_rungs classifies it
intentional with the declared reason). These tests lock the decision matrix; derive_rungs'
classification of the resulting skip is already covered in test_results.py.
"""
from __future__ import annotations
import os
import sys
from types import SimpleNamespace
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
import run_recipe_ci # noqa: E402
from harness import lifecycle # noqa: E402
ALL = {"install", "upgrade", "backup", "restore", "custom"}
def _meta(expected_na=None, upgrade_base_version=None):
return SimpleNamespace(EXPECTED_NA=expected_na, UPGRADE_BASE_VERSION=upgrade_base_version)
def test_default_prev_published(monkeypatch):
# upgrade in stages, ≥2 published versions, nothing declared → recipe_versions[-2]
monkeypatch.setattr(lifecycle, "previous_version", lambda r: "0.1.0+v1")
assert run_recipe_ci.upgrade_base(ALL, _meta(), "somerecipe") == "0.1.0+v1"
def test_single_published_version_no_base(monkeypatch):
monkeypatch.setattr(lifecycle, "previous_version", lambda r: None)
assert run_recipe_ci.upgrade_base(ALL, _meta(), "somerecipe") is None
def test_upgrade_not_in_stages_no_base(monkeypatch):
monkeypatch.setattr(
lifecycle,
"previous_version",
lambda r: (_ for _ in ()).throw(AssertionError("not consulted")),
)
assert run_recipe_ci.upgrade_base(ALL - {"upgrade"}, _meta(), "somerecipe") is None
def test_upgrade_base_version_override_wins(monkeypatch):
monkeypatch.setattr(
lifecycle,
"previous_version",
lambda r: (_ for _ in ()).throw(AssertionError("not consulted")),
)
meta = _meta(upgrade_base_version="0.7.0+3.3.1")
assert run_recipe_ci.upgrade_base(ALL, meta, "discourse") == "0.7.0+3.3.1"
def test_expected_na_upgrade_suppresses_base(monkeypatch):
# bluesky-pds shape: published versions exist but are undeployable — declared EXPECTED_NA
# upgrade → NO base (single deploy is the PR head), even though previous_version would
# return one and even if UPGRADE_BASE_VERSION is set (the declaration is the stronger,
# documented fact).
monkeypatch.setattr(lifecycle, "previous_version", lambda r: "0.1.1+v0.4")
declared = {"upgrade": "no deployable upgrade base (moving tag republished)"}
assert run_recipe_ci.upgrade_base(ALL, _meta(expected_na=declared), "bluesky-pds") is None
assert (
run_recipe_ci.upgrade_base(
ALL, _meta(expected_na=declared, upgrade_base_version="0.2.0+v0.4"), "bluesky-pds"
)
is None
)
def test_expected_na_other_rung_does_not_suppress(monkeypatch):
# an EXPECTED_NA for backup_restore (custom-html-tiny shape) must NOT touch the upgrade base
monkeypatch.setattr(lifecycle, "previous_version", lambda r: "0.1.0+v1")
meta = _meta(expected_na={"backup_restore": "stateless"})
assert run_recipe_ci.upgrade_base(ALL, meta, "custom-html-tiny") == "0.1.0+v1"