Compare commits

...
Author SHA1 Message Date
autonomic-bot 0a229ac016 opencode: run task-tool subagents on deepseek-v4-pro
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
Main/driving sessions stay glm-5.2 (set per-launch via --model); the built-in
'general' agent used by the task tool runs opencode/deepseek-v4-pro: ~3-5x cheaper
per token with near-free cache hits, and the weekly /upgrade-all spends most of its
budget in per-recipe subagents.

Placement matters and was got wrong once (2026-08-04 config sat in the orchestrator
repo and never bound; the whole 2026-08-07 run billed as glm): sessions launched by
launch-upgrader.py pass no --dir, so they inherit the opencode serve process's
project (WorkingDirectory=/srv/cc-ci-orch/cc-ci = THIS repo), and task-tool
subagents inherit their parent session's directory. This file is therefore the
project config those subagents actually resolve.

Verified end-to-end 2026-08-10 with the launcher's exact invocation: parent session
modelID=glm-5.2, spawned subagent modelID=deepseek-v4-pro (read back from the
opencode session DB, not from config inspection).
2026-08-10 15:57:04 +00:00
autonomic-bot de1eb1ca75 Merge pull request 'test(discourse): UPGRADE_BASE_FLOOR — exclude structurally-invalid upgrade bases' (#15) from test/discourse-upgrade-base-floor-20260804 into main
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2026-08-04 17:57:46 +00:00
autonomic-bot 877aea3814 test(discourse): version-agnostic official-image assertion
test_head_runs_official_image_not_bitnamilegacy hardcoded the migration-era pin
discourse/discourse:3.5.3 and went stale on the first legitimate app bump
(2026.7.1, weekly 2026-08-03 — caught by verify run 2: the upgrade converged,
head image was discourse/discourse:2026.7.1, only the frozen pin failed). The
guarded property is the image FAMILY (official vs bitnamilegacy), not a frozen
version — now asserts the discourse/discourse: prefix. Not weakened: the
bitnami-leak check + official-prefix check together still assert exactly the
migration faithfulness; the concrete head pin is exercised by the deploy.
2026-08-04 17:46:33 +00:00
autonomic-bot ae40545491 meta: register UPGRADE_BASE_FLOOR key (phase basefloor)
continuous-integration/drone/push Build is failing
The strict recipe_meta key registry rejected the new declaration (caught by
verify run 1). str-typed, default None; full semantics documented on the key.
2026-08-04 17:37:02 +00:00
autonomic-bot 5086b2f8bb test(discourse): UPGRADE_BASE_FLOOR — exclude structurally-invalid upgrade bases
continuous-integration/drone/push Build is failing
The 0.8.x->1.0.0 discourse recipe family switched app bitnami->official AND db
pgvector/pg17->discourse/postgres:pg18. That db-family change is a structural
break (bitnami cluster lacks the discourse role; pg_upgrade preserves-not-creates
roles) with NO supported in-place path. The dynamic base resolver's step-back
kept selecting 0.8.1+3.5.0 (newest tag below the unbumped 1.0.0+3.5.3 label) and
the upgrade tier red'd twice on this unsupported path (drone #1165, #1171 —
classified stale-test both times; recipe verified green on the real
official->official path).

Adds UPGRADE_BASE_FLOOR (phase basefloor) to resolve_upgrade_base: a recipe_meta
declaration naming the first post-break published version. Resolution stays
fully dynamic (this is NOT the removed sec2.G static pin): the floor only
EXCLUDES below-floor candidates (canonical, step-back, no-canonical fallback);
when no >=floor predecessor exists the tier records a DECLARED skip, never a
silent pass. main-tip fallback unaffected (post-break by construction).

tests/discourse/recipe_meta.py declares UPGRADE_BASE_FLOOR = 1.0.0+3.5.3 with
the full rationale. Unit-verified: head=1.0.0+3.5.3 -> declared skip (was:
0.8.1+3.5.0 wrong pick); post-release head=2026.x -> base 1.0.0+3.5.3 (the real
migration path). No assertion weakened - below-floor in-place upgrades were
never supported coverage.
2026-08-04 17:15:37 +00:00
autonomic-bot 5327a24faa Merge pull request 'enroll(wordpress): test suite + bridge POLL_REPOS entry' (#14) from test/wordpress-enroll-20260803 into main
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
2026-08-04 17:01:35 +00:00
5 changed files with 89 additions and 19 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://opencode.ai/config.json",
"agent": {
"general": {
"model": "opencode/deepseek-v4-pro"
}
}
}
+11
View File
@@ -132,6 +132,17 @@ KEYS: tuple[Key, ...] = (
"Callable `(ctx)` invoked after UPGRADE_EXTRA_ENV env_set but before `abra secret generate --all` in the upgrade path. Use to pre-insert secrets that `generate --all` would produce with wrong format (e.g. when the .env.sample spec is commented out).", "Callable `(ctx)` invoked after UPGRADE_EXTRA_ENV env_set but before `abra secret generate --all` in the upgrade path. Use to pre-insert secrets that `generate --all` would produce with wrong format (e.g. when the .env.sample spec is commented out).",
hook_params=("ctx",), hook_params=("ctx",),
), ),
Key(
"UPGRADE_BASE_FLOOR",
"str",
None,
"Declared STRUCTURAL breaking boundary for the upgrade tier (phase basefloor): the first "
"post-break published version tag. Bases strictly below it are excluded from resolution "
"(canonical / step-back / no-canonical fallback) because an in-place upgrade across the "
"boundary is not supported upstream (e.g. a db-family change). When no ≥-floor predecessor "
"exists the tier records a DECLARED skip. NOT a static base pin (§2.G stays removed) — "
"resolution remains dynamic above the floor.",
),
# (CHAOS_BASE_DEPLOY, OIDC_AT_INSTALL and SKIP_GENERIC were deleted in restructure P2: # (CHAOS_BASE_DEPLOY, OIDC_AT_INSTALL and SKIP_GENERIC were deleted in restructure P2:
# compose.ccci.yml is first-class + auto-chaos; install-time deps wiring is the only mode; # compose.ccci.yml is first-class + auto-chaos; install-time deps wiring is the only mode;
# the generic floor is suppressible only via the dev-only CCCI_SKIP_GENERIC* env form.) # the generic floor is suppressible only via the dev-only CCCI_SKIP_GENERIC* env form.)
+48 -11
View File
@@ -151,8 +151,30 @@ def resolve_upgrade_base(
flush=True, flush=True,
) )
return BasePlan("skip", None, None, f"declared EXPECTED_NA[upgrade]: {declared}") return BasePlan("skip", None, None, f"declared EXPECTED_NA[upgrade]: {declared}")
# UPGRADE_BASE_FLOOR (phase basefloor): a recipe_meta declaration marking a STRUCTURAL breaking
# boundary — published versions strictly below the floor are not valid in-place upgrade sources
# (e.g. discourse 0.8.x→1.0.0 changed the db family bitnami/pgvector → discourse/postgres; the
# data layout+roles are incompatible, upstream supports no in-place path across it). This is NOT
# the removed UPGRADE_BASE_VERSION pin (§2.G): resolution stays fully dynamic — the floor only
# EXCLUDES structurally-impossible bases, and when no candidate ≥ floor exists the tier records
# a DECLARED skip (never a silent pass). Never weakens: below-floor upgrades were never a
# supported path, so no real coverage is lost.
floor = getattr(meta, "UPGRADE_BASE_FLOOR", None)
def _below_floor(version: str) -> bool:
return bool(floor) and warm_reconcile.version_key(version) < warm_reconcile.version_key(
floor
)
skip_canonicals = settings_mod.get().skip_canonicals_for_upgrade skip_canonicals = settings_mod.get().skip_canonicals_for_upgrade
rec = canonical.read_registry(recipe) rec = canonical.read_registry(recipe)
if rec and rec.get("version") and not skip_canonicals and _below_floor(rec["version"]):
print(
f"== upgrade tier: last-green canonical {rec['version']} is below the declared "
f"UPGRADE_BASE_FLOOR {floor} (structural break) — excluded as a base",
flush=True,
)
rec = None
if rec and rec.get("version") and not skip_canonicals: if rec and rec.get("version") and not skip_canonicals:
canon = rec["version"] canon = rec["version"]
same = head_version is not None and warm_reconcile.version_key( same = head_version is not None and warm_reconcile.version_key(
@@ -168,10 +190,20 @@ def resolve_upgrade_base(
f"last-green (warm canonical, status={rec.get('status')})", f"last-green (warm canonical, status={rec.get('status')})",
) )
# canonical == head version → deploying it would be a same-version no-op. Step back to the # canonical == head version → deploying it would be a same-version no-op. Step back to the
# newest published version strictly older than the head (phase samever). # newest published version strictly older than the head (phase samever). Candidates below a
older = warm_reconcile.newest_older_version( # declared UPGRADE_BASE_FLOOR are excluded (phase basefloor — structurally invalid bases).
warm_reconcile.recipe_tags(recipe), head_version _tags = warm_reconcile.recipe_tags(recipe)
) if floor:
_tags = [t for t in _tags if not _below_floor(t)]
older = warm_reconcile.newest_older_version(_tags, head_version)
if older is None and floor:
return BasePlan(
"skip",
None,
None,
f"declared UPGRADE_BASE_FLOOR {floor}: no published predecessor ≥ floor below "
f"head {head_version} (all older tags cross a structural break)",
)
if older: if older:
return BasePlan( return BasePlan(
"version", "version",
@@ -189,10 +221,12 @@ def resolve_upgrade_base(
# No canonical in play — none recorded, OR SKIP_CANONICALS_FOR_UPGRADE=true (canonical lookup # No canonical in play — none recorded, OR SKIP_CANONICALS_FOR_UPGRADE=true (canonical lookup
# bypassed entirely, behaving as if none exists). Improved fallback (phase settings §2.C): prefer # bypassed entirely, behaving as if none exists). Improved fallback (phase settings §2.C): prefer
# a REAL published predecessor (newest release tag < head) over the raw main-tip. # a REAL published predecessor (newest release tag < head) over the raw main-tip.
return _no_canonical_base(recipe, head_ref, head_version) return _no_canonical_base(recipe, head_ref, head_version, floor=floor)
def _no_canonical_base(recipe: str, head_ref: str | None, head_version: str | None) -> BasePlan: def _no_canonical_base(
recipe: str, head_ref: str | None, head_version: str | None, floor: str | None = None
) -> BasePlan:
"""Upgrade base when no canonical is used (none recorded, its promote failed, or """Upgrade base when no canonical is used (none recorded, its promote failed, or
SKIP_CANONICALS_FOR_UPGRADE is true). Release-tag-first fallback (phase settings §2.C): SKIP_CANONICALS_FOR_UPGRADE is true). Release-tag-first fallback (phase settings §2.C):
1. most recent release TAG with version strictly older than the PR head — a clean published 1. most recent release TAG with version strictly older than the PR head — a clean published
@@ -202,11 +236,14 @@ def _no_canonical_base(recipe: str, head_ref: str | None, head_version: str | No
3. skip — no predecessor (no older tag and head == main-tip, or no main at all). 3. skip — no predecessor (no older tag and head == main-tip, or no main at all).
This replaces the old jump-straight-to-main-tip path, so an un-promoted recipe upgrades from a real This replaces the old jump-straight-to-main-tip path, so an un-promoted recipe upgrades from a real
release base instead of a possibly-untagged WIP commit.""" release base instead of a possibly-untagged WIP commit."""
older = ( _tags = warm_reconcile.recipe_tags(recipe)
warm_reconcile.newest_older_version(warm_reconcile.recipe_tags(recipe), head_version) if floor:
if head_version # phase basefloor: exclude structurally-invalid bases below the declared floor; the
else None # main-tip fallback below remains available (it is post-break by definition of the
) # declaration — the floor names the first post-break published version).
_fk = warm_reconcile.version_key(floor)
_tags = [t for t in _tags if warm_reconcile.version_key(t) >= _fk]
older = warm_reconcile.newest_older_version(_tags, head_version) if head_version else None
if older: if older:
return BasePlan( return BasePlan(
"version", "version",
+15 -5
View File
@@ -23,11 +23,21 @@ HTTP_TIMEOUT = 1200
# #
# UPGRADE-tier BASE (phase prevb — DYNAMIC, no hardcoded UPGRADE_BASE_VERSION): the base the head # UPGRADE-tier BASE (phase prevb — DYNAMIC, no hardcoded UPGRADE_BASE_VERSION): the base the head
# upgrades from is resolved at run time — last-green (warm canonical) → fallback target-branch (`main`) # upgrades from is resolved at run time — last-green (warm canonical) → fallback target-branch (`main`)
# tip → else skip (run_recipe_ci.resolve_upgrade_base). discourse has no warm canonical, so the base is # tip → else skip (run_recipe_ci.resolve_upgrade_base).
# the `main` tip = bitnamilegacy/discourse:3.5.0, which deploys clean (bitnamilegacy exists) with NO #
# `previous/` repair needed. The PR head (recipe-maintainers/discourse#4) switches app to the official # UPGRADE_BASE_FLOOR (phase basefloor, 2026-08-04): the 0.8.x→1.0.0 recipe family switched the app
# `discourse/discourse:3.5.3` and drops the sidekiq service, so the upgrade tier now exercises the REAL # bitnamilegacy/discourse → official discourse/discourse AND the db pgvector/pgvector:pg17 →
# bitnamilegacy→official image migration the PR claims to support. # discourse/postgres:pg18. That db-family change is a structural break: the bitnami cluster has no
# `discourse` role and pg_upgrade preserves-not-creates roles, so an in-place 0.8.x→1.x deploy can
# NEVER converge (app FATALs `role "discourse" does not exist`, swarm rolls back) — upstream ships
# no in-place path across it. Without the floor, the resolver's step-back/fallback selected
# 0.8.1+3.5.0 (newest tag below the head label) and the upgrade tier red'd on this unsupported
# path twice (drone #1165 2026-07-31 diagnosis, #1171/weekly 2026-08-03 — both classified
# stale-test, recipe verified green on the real official→official path). Declaring the floor keeps
# resolution dynamic and only excludes the structurally-impossible bases; when no ≥-floor
# predecessor exists the tier records a DECLARED skip (never a silent pass). No assertion weakened:
# below-floor in-place upgrades were never supported coverage.
UPGRADE_BASE_FLOOR = "1.0.0+3.5.3"
# #
# compose.ccci.yml is now the ENVIRONMENTAL overlay (all deploys): only app.deploy.update_config.order: # compose.ccci.yml is now the ENVIRONMENTAL overlay (all deploys): only app.deploy.update_config.order:
# stop-first (node memory reality on the upgrade crossover — see its header). The version-specific # stop-first (node memory reality on the upgrade crossover — see its header). The version-specific
+7 -3
View File
@@ -6,7 +6,11 @@ migration was never tested. With the version-specific config removed from the al
and the dynamic base (last-green/main = bitnamilegacy:3.5.0) deployed only as the *base*, the upgrade and the dynamic base (last-green/main = bitnamilegacy:3.5.0) deployed only as the *base*, the upgrade
chaos redeploy must land the PR head UNMODIFIED. This overlay asserts exactly that, post-upgrade: chaos redeploy must land the PR head UNMODIFIED. This overlay asserts exactly that, post-upgrade:
1. the running `app` service image IS the official discourse/discourse:3.5.3 — NOT bitnamilegacy; 1. the running `app` service image IS from the official `discourse/discourse` repository —
NOT bitnamilegacy. (Version-agnostic since 2026-08-04: the original assertion hardcoded the
migration-era pin `:3.5.3` and went stale on the first legitimate app bump (2026.7.1, weekly
2026-08-03). The property this test guards is the IMAGE FAMILY — official vs bitnami — not a
frozen version; the exact head pin is already exercised by the deploy itself.)
2. the `sidekiq` service the PR deletes is GONE from the deployed stack. 2. the `sidekiq` service the PR deletes is GONE from the deployed stack.
If either fails, the head did not really run (the overlay leaked onto it) → RED. Assertion-only, If either fails, the head did not really run (the overlay leaked onto it) → RED. Assertion-only,
@@ -26,8 +30,8 @@ def test_head_runs_official_image_not_bitnamilegacy(live_app):
f"app image is {image!r} — the bitnamilegacy base leaked onto the PR head " f"app image is {image!r} — the bitnamilegacy base leaked onto the PR head "
"(the version-specific overlay was applied to the head, the prevb bug)" "(the version-specific overlay was applied to the head, the prevb bug)"
) )
assert image.startswith("discourse/discourse:3.5.3"), ( assert image.startswith("discourse/discourse:"), (
f"app image is {image!r}, expected the PR head's official discourse/discourse:3.5.3 " f"app image is {image!r}, expected the PR head's official discourse/discourse image "
"— the head's image migration was not exercised" "— the head's image migration was not exercised"
) )