"""`artifacts.sh identity` is what decides whether a build gets skipped. Milestone 313 step 4: build.yml asks the registry for `:` and, on a hit, publishes NO new bytes — it repoints the channel and date tags at the manifest already there. So the identity has to be a true name for the content. Both ways of getting it wrong are silent at build time and only surface in production: * **too coarse** — two genuinely different images share an identity, so the second one never gets built and its tags point at the first one's bytes. The live case is FC_CHANNEL: a `dev` and a `main` build of one revision differ, and collapsing them ships an instance that reports the wrong channel forever. * **too fine** — the identity moves when the content did not, nothing ever hits, and step 4 buys nothing. A commit sha would do exactly this. The Dockerfiles are read here rather than trusted, because the coarse direction appears the moment someone adds a build-arg without touching `CHANNELLED`. """ from __future__ import annotations import re import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent # Only image artifacts have an identity — the extension is cached as an # ext- Forgejo release, not a registry tag. IMAGE_ARTIFACTS = { "web": "Dockerfile", "ml": "Dockerfile.ml", "agent": "agent/Dockerfile", } CHANNELS = ("main", "dev") # docker's own tag grammar: [A-Za-z0-9_][A-Za-z0-9._-]{0,127} _TAG = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$") # `ARG FC_CHANNEL` in a Dockerfile means build.yml passes a per-channel value # in, so the channel is part of what the image IS. _ARG_CHANNEL = re.compile(r"^\s*ARG\s+FC_CHANNEL\b", re.MULTILINE) def identity(artifact: str, channel: str | None = None) -> subprocess.CompletedProcess: cmd = ["sh", str(ROOT / "scripts" / "artifacts.sh"), "identity", artifact] if channel is not None: cmd.append(channel) return subprocess.run(cmd, capture_output=True, text=True, cwd=ROOT) def ok(artifact: str, channel: str | None = None) -> str: proc = identity(artifact, channel) assert proc.returncode == 0, f"identity {artifact} {channel}: {proc.stderr}" return proc.stdout.strip() def bakes_the_channel(artifact: str) -> bool: return bool(_ARG_CHANNEL.search((ROOT / IMAGE_ARTIFACTS[artifact]).read_text())) @pytest.mark.parametrize("artifact", sorted(IMAGE_ARTIFACTS)) def test_channel_dependence_matches_the_dockerfile(artifact): """The coarse direction, caught at its source. Whether the channel belongs in the identity is not a preference — it is dictated by whether the Dockerfile takes it as a build-arg. Adding an `ARG FC_CHANNEL` to another image without adding it to `CHANNELLED` would make its dev and main builds collide, and nothing else would notice. """ per_channel = {c: ok(artifact, c) for c in CHANNELS} differs = len(set(per_channel.values())) > 1 if bakes_the_channel(artifact): assert differs, ( f"{IMAGE_ARTIFACTS[artifact]} declares ARG FC_CHANNEL, so a dev " f"build and a main build of one revision are different images — " f"but both derive the identity {per_channel['main']!r}. The main " f"build would reuse the dev image and report the wrong channel. " f"Add {artifact!r} to CHANNELLED in scripts/artifacts.sh." ) else: assert not differs, ( f"{IMAGE_ARTIFACTS[artifact]} takes no channel build-arg, so one " f"revision is one image and a merge to main should reuse what dev " f"already built — but the identity differs per channel " f"({per_channel}), so every merge rebuilds it for nothing. Remove " f"{artifact!r} from CHANNELLED in scripts/artifacts.sh." ) @pytest.mark.parametrize("artifact", sorted(IMAGE_ARTIFACTS)) def test_identity_tracks_the_artifacts_own_revision(artifact): """The fine direction: the identity must be the revision, not the push. `revision` is the commit this artifact's shipped files last changed in, so it holds still across pushes that did not touch it. Anything derived from HEAD instead would move every push and never hit the registry. """ rev = subprocess.run( ["sh", str(ROOT / "scripts" / "artifacts.sh"), "revision", artifact], capture_output=True, text=True, check=True, cwd=ROOT, ).stdout.strip() value = ok(artifact, "main") assert rev and rev in value, ( f"identity {value!r} does not contain the {artifact} revision {rev!r}" ) @pytest.mark.parametrize("artifact", sorted(IMAGE_ARTIFACTS)) def test_identity_is_a_legal_docker_tag(artifact): """It is pushed as a tag, so an illegal one fails at the registry — after the build has already run.""" for channel in CHANNELS: value = ok(artifact, channel) assert _TAG.match(value), f"{value!r} is not a valid docker tag" def test_a_channelled_artifact_refuses_an_unqualified_identity(): """Refusing beats defaulting. If `identity web` quietly returned the unqualified `r-`, a workflow that forgot to pass the channel would publish one image under a name both channels then reuse — the exact collision the CHANNELLED list exists to prevent, reintroduced by an omission rather than by an edit.""" proc = identity("web") assert proc.returncode != 0, ( "identity web returned a value with no channel: " f"{proc.stdout.strip()!r}" ) def test_the_extension_has_no_image_identity(): """It is cached as an ext- release asset, and its cache key is the version. Answering with a plausible image tag would invite a second, divergent cache.""" proc = identity("extension", "main") assert proc.returncode != 0 assert "ext-" in proc.stderr