fix(gtea): UPGRADE_SECRET_PREP hook — pre-insert lfs_jwt_secret with correct 43-char format
Some checks failed
continuous-integration/drone/push Build is failing

Blocker 4 fix: abra `secret generate --all` uses .env.sample for length hints; the
lfs-plain-gitea PR has SECRET_LFS_JWT_SECRET_VERSION=v1 COMMENTED OUT, so abra produces
a wrong-length secret. gitea requires exactly 43 chars (32 bytes base64 URL-safe); wrong
length → gitea fatals trying to save the JWT secret to the read-only Docker Config
app.ini → health check fails → swarm rolls back.

Fix: new UPGRADE_SECRET_PREP hook (meta.py) called before `abra secret generate --all`
in the upgrade path. abra's `--all` is idempotent (skips existing secrets), so the
correctly pre-inserted secret survives. gitea's recipe_meta.py implements the hook using
`docker secret create` directly to guarantee correct format regardless of .env.sample.

Also consumes machine-docs/BUILDER-INBOX.md (Adversary Blocker 4 digest).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-06-15 21:46:28 +00:00
parent 1efab2e1e6
commit d832b353e4
7 changed files with 72 additions and 111 deletions

View File

@ -60,6 +60,45 @@ def UPGRADE_EXTRA_ENV(ctx):
}
def UPGRADE_SECRET_PREP(ctx):
"""Pre-insert lfs_jwt_secret with the correct 43-char base64 URL-safe format before
`abra secret generate --all` runs. The lfs-plain-gitea PR's .env.sample has the
SECRET_LFS_JWT_SECRET_VERSION=v1 spec COMMENTED OUT, so abra uses a wrong default length;
gitea requires exactly 43 chars (32 bytes) or it fatals on the read-only app.ini."""
if not _lfs_enabled():
return
import base64
import subprocess
env_path = _os.path.expanduser(f"~/.abra/servers/default/{ctx.domain}.env")
stack_name = None
try:
with open(env_path) as fh:
for line in fh:
if line.startswith("STACK_NAME="):
stack_name = line.split("=", 1)[1].strip().strip('"').strip("'")
except OSError:
pass
if not stack_name:
raise RuntimeError(f"UPGRADE_SECRET_PREP: STACK_NAME not found in {env_path}")
docker_secret = f"{stack_name}_lfs_jwt_secret_v1"
value = base64.urlsafe_b64encode(_os.urandom(32)).rstrip(b"=").decode()
subprocess.run(["docker", "secret", "rm", docker_secret], capture_output=True)
result = subprocess.run(
["docker", "secret", "create", docker_secret, "-"],
input=value,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(
f"UPGRADE_SECRET_PREP: docker secret create {docker_secret}: {result.stderr.strip()}"
)
print(f" gitea upgrade: pre-created {docker_secret} (43-char lfs_jwt_secret)", flush=True)
def EXTRA_ENV(ctx):
lfs = _lfs_enabled()
compose_file = "compose.yml:compose.sqlite3.yml"

View File

@ -65,6 +65,7 @@ def test_missing_meta_yields_spec_baseline(tmp_path):
assert meta.DEPS == []
assert meta.WARM_CANONICAL is False
assert meta.SCREENSHOT is None
assert meta.UPGRADE_SECRET_PREP is None
assert meta_mod.non_default(meta) == {}
@ -73,9 +74,9 @@ def test_registry_field_set_matches_dataclass():
import dataclasses
assert [f.name for f in dataclasses.fields(RecipeMeta)] == [k.name for k in KEYS]
# the 14 final keys, no more (the 3 P2-deleted legacy keys are gone from the registry,
# the 15 final keys, no more (the 3 P2-deleted legacy keys are gone from the registry,
# so any recipe_meta still setting them hard-fails the typo gate)
assert len(KEYS) == 14
assert len(KEYS) == 15
assert not [k for k in KEYS if k.deprecated]
for gone in ("CHAOS_BASE_DEPLOY", "OIDC_AT_INSTALL", "SKIP_GENERIC"):
assert gone not in {k.name for k in KEYS}