diff --git a/runner/harness/abra.py b/runner/harness/abra.py index c199c51..7bd1d0b 100644 --- a/runner/harness/abra.py +++ b/runner/harness/abra.py @@ -83,70 +83,30 @@ def recipe_checkout(recipe: str, version: str) -> None: subprocess.run(["git", "-C", path, "checkout", "--quiet", version], check=True) -def normalize_recipe_tags(recipe: str) -> int: - """Make abra's pinned-deploy lint pass (R014 'only annotated tags used for recipe version') when - an upstream coop-cloud recipe ships a stray LIGHTWEIGHT version tag (e.g. lasuite-meet's - `0.3.0+v1.16.0`). Such a tag FATAs `abra app deploy ` lint for the WHOLE recipe, - blocking the upgrade tier's prev-version base deploy. +def has_lightweight_version_tags(recipe: str) -> bool: + """True if the recipe's local checkout has any LIGHTWEIGHT (non-annotated) version tag. - Two-step, because abra force-fetches tags (`git fetch --tags --force`) from `origin` before it - lints — so re-annotating in place alone is reverted by abra. We therefore: - 1. Re-create each lightweight tag as ANNOTATED at the SAME commit (no deployed content changes — - only the tag object type). - 2. Snapshot the corrected recipe into a local bare repo and repoint `origin` at it, so abra's - pre-lint force-fetch pulls the *annotated* tag (verified: R014 then passes and the annotation - sticks). The deployed commit is identical; this only corrects tag hygiene abra insists on. - No-op for recipes whose tags are already annotated (most). Returns the count re-annotated.""" + Some upstream coop-cloud recipes ship a stray lightweight tag (e.g. lasuite-meet's + `0.3.0+v1.16.0`). abra's pinned (non-chaos) deploy runs `abra recipe lint`, which FATAs R014 + ('only annotated tags used for recipe version') for the WHOLE recipe — blocking the upgrade tier's + prev-version base deploy. (Re-annotating locally doesn't help: abra force-fetches tags from origin + before linting and reverts it; repointing origin to a local mirror tripped a go-git + 'reference not found'.) The caller (deploy_app) uses this to fall back to a chaos base deploy + (which skips lint and deploys the explicitly-checked-out pinned version — see lifecycle.deploy_app). + Read-only: just `git tag` + `cat-file -t`; no fetch/mutation, so it can't trigger abra's revert.""" import os path = os.path.expanduser(f"~/.abra/recipes/{recipe}") tags = subprocess.run( ["git", "-C", path, "tag", "-l"], capture_output=True, text=True ).stdout.split() - git_env = dict( - os.environ, - GIT_COMMITTER_NAME="cc-ci", - GIT_COMMITTER_EMAIL="ci@cc-ci.local", - GIT_AUTHOR_NAME="cc-ci", - GIT_AUTHOR_EMAIL="ci@cc-ci.local", - ) - fixed = 0 for t in tags: objtype = subprocess.run( ["git", "-C", path, "cat-file", "-t", t], capture_output=True, text=True ).stdout.strip() - if objtype != "commit": # already annotated (objtype == "tag") - continue - commit = subprocess.run( - ["git", "-C", path, "rev-list", "-n", "1", t], capture_output=True, text=True - ).stdout.strip() - if not commit: - continue - subprocess.run( - ["git", "-C", path, "tag", "-a", "-f", "-m", f"cc-ci: annotate {t} for R014", t, commit], - check=True, - env=git_env, - ) - fixed += 1 - if fixed: - # Repoint origin at a local bare snapshot carrying the annotated tags, so abra's pre-lint - # `git fetch --tags --force` (which otherwise reverts the in-place re-annotation to the - # upstream lightweight tag) pulls the corrected tags instead. - bare = os.path.expanduser(f"~/.abra/recipes/.{recipe}-ccci-fixed.git") - subprocess.run(["rm", "-rf", bare], check=False) - # --mirror (not --bare): copies ALL refs incl. refs/heads/main, so abra's later git ops - # (`app secret insert`, deploy) that fetch from origin find every ref ("reference not found" - # if main is missing). The annotated tags ride along. - subprocess.run(["git", "clone", "--quiet", "--mirror", path, bare], check=True) - subprocess.run( - ["git", "-C", path, "remote", "set-url", "origin", f"file://{bare}"], check=True - ) - print( - f" normalize_recipe_tags({recipe}): re-annotated {fixed} lightweight tag(s) + repointed " - f"origin → local bare (R014; abra force-fetch now preserves annotation)", - flush=True, - ) - return fixed + if objtype == "commit": # lightweight (annotated tags are objtype "tag") + return True + return False def env_set(domain: str, key: str, value: str) -> None: diff --git a/runner/harness/lifecycle.py b/runner/harness/lifecycle.py index 552bb75..dc06299 100644 --- a/runner/harness/lifecycle.py +++ b/runner/harness/lifecycle.py @@ -145,12 +145,22 @@ def deploy_app( # on-disk compose/.env match, and deploy NON-chaos below (chaos ignores the pin → deployed LATEST, # Adversary F1d-2). Chaos is correct ONLY for the version=None case (deploy the current PR-head # checkout). Order matters: checkout before secret_generate (-C) so secrets match the pinned tree. + chaos = version is None if version: abra.recipe_checkout(recipe, version) - # A pinned (non-chaos) deploy runs `abra recipe lint`; normalize any stray lightweight - # upstream tags to annotated so R014 doesn't FATA the whole recipe (changes no deployed - # content — see abra.normalize_recipe_tags). No-op for recipes with all-annotated tags. - abra.normalize_recipe_tags(recipe) + # A pinned (non-chaos) deploy runs `abra recipe lint`, which FATAs R014 ('only annotated + # tags') if the upstream recipe ships a stray lightweight version tag (e.g. lasuite-meet's + # 0.3.0+v1.16.0). In that case deploy the EXPLICITLY-checked-out pinned version with chaos: + # chaos skips lint and deploys the current checkout (we just checked out `version`), so it + # still deploys the intended pinned version — not LATEST (the F1d-2 hazard was a *missing* + # checkout, which recipe_checkout above fixes). No-op for all-annotated recipes (stays pinned). + if abra.has_lightweight_version_tags(recipe): + print( + f" deploy_app({recipe}@{version}): lightweight upstream tag present → chaos base " + "deploy of the checked-out pinned version (skips R014 lint; not LATEST)", + flush=True, + ) + chaos = True # Pin DOMAIN to the run domain explicitly. `abra app new -D` fills it for recipes whose # .env.sample uses a literal placeholder, but NOT for ones using a `{{ .Domain }}` Go-template # (this abra version leaves it unexpanded → deploy fails "can't evaluate field Domain"). Setting @@ -163,7 +173,7 @@ def deploy_app( abra.secret_generate(domain) if install_steps_hook: _run_install_steps(install_steps_hook, recipe, domain) - abra.deploy(domain, chaos=(version is None), timeout=deploy_timeout) + abra.deploy(domain, chaos=chaos, timeout=deploy_timeout) def _stack_name(domain: str) -> str: