Commit 6f783bbe authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

ci: select main artifacts for coordinated release acceptance

parent aa6b57ec
Loading
Loading
Loading
Loading
+21 −4
Changes for ansible/tests/unit/test_candidate_resolution.py: 21 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -27,7 +27,8 @@ def wheel(version, contents=b"runtime = 1\n"):


class Registry:
    def __init__(self, fault=None):
    def __init__(self, fault=None, candidate_ref="develop"):
        self.candidate_ref = candidate_ref
        self.fault = fault
        self.names = {1: "oeg", 2: "fm", 3: "srm", 4: "sdk", 5: "helm"}
        self.heads = {name: str(i) * 40 for i, name in self.names.items()}
@@ -56,11 +57,13 @@ class Registry:
        identity = int(path.split("/")[1]);name = self.names[identity]
        if path.endswith("/repository/branches/test/current-consumer"):
            result = {"commit": {"id": "6" * 40}}
        elif path.endswith("/repository/branches/develop"):
        elif path.endswith("/repository/branches/" + self.candidate_ref):
            head = "a" * 40 if self.fault == "moving-head" and self.reads[path] > 1 else self.heads[name]
            result = {"commit": {"id": head}}
        elif path.endswith("/pipelines"):
            assert query["sha"] == [self.heads[name]]
            if name != "sdk":
                assert query["ref"] == [self.candidate_ref]
            result = [] if self.fault == "missing-pipeline" else [
                {"id": 100 + identity, "sha": self.heads[name], "status": "success", "web_url": url}]
        elif path.endswith("/jobs"):
@@ -73,6 +76,7 @@ class Registry:
            result = [{"id": identity, "path": self.paths[name],
                       "location": "labs.etsi.org:5050/" + self.paths[name]}]
        elif "/registry/repositories/" in path:
            assert path.endswith("/tags/ci-" + self.candidate_ref + "-" + self.heads[name][:8])
            result = {"digest": "sha256:" + str(identity) * 64}
        elif "/repository/files/" in path:
            assert query["ref"] == [self.heads["srm"]]
@@ -87,12 +91,17 @@ class Registry:
        return json.dumps(result).encode()


def test_latest_image_refs_and_actual_locked_sdk_are_resolved_together(tmp_path):
@pytest.mark.parametrize("candidate_ref", ["develop", "main"])
def test_latest_image_refs_and_actual_locked_sdk_are_resolved_together(tmp_path, candidate_ref):
    original = {"images": {}, "checkpoint": "terminated", "acceptance": {"sdk": {}}}
    if candidate_ref == "main":
        original["candidate_ref"] = candidate_ref
    unchanged = deepcopy(original)
    registry = Registry()
    registry = Registry(candidate_ref=candidate_ref)
    profile, evidence, sdk_wheel = resolve_candidate(original, tmp_path, fetch=registry)
    assert original == unchanged
    assert evidence["ref"] == candidate_ref
    assert all(":ci-" + candidate_ref + "-" in image for image in profile["images"].values())
    assert profile["chart_revision"] == registry.heads["helm"]
    assert profile["images"]["srm"].endswith("@sha256:" + "3" * 64)
    assert profile["acceptance"]["sdk"]["version"] == "2.0.0rc4"
@@ -105,3 +114,11 @@ def test_latest_image_refs_and_actual_locked_sdk_are_resolved_together(tmp_path)
def test_stale_unverified_or_changed_candidates_are_rejected(tmp_path, fault):
    with pytest.raises((ValueError, RuntimeError)):
        resolve_candidate({"acceptance": {"sdk": {}}}, tmp_path, fetch=Registry(fault))


def test_unknown_candidate_ref_is_rejected_before_any_lookup(tmp_path):
    def unexpected_lookup(url):
        pytest.fail("An invalid ref must not query a project or fall back")

    with pytest.raises(ValueError, match="candidate_ref"):
        resolve_candidate({"candidate_ref": "feature/unreviewed"}, tmp_path, fetch=unexpected_lookup)
+5 −2
Changes for ci/lab/README.md: 5 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -98,8 +98,11 @@ OOP_LAB_REFRESH_CANDIDATE=1 sh ci/run_platform_lab.sh \
```

This mode selects source commits, successful image-build jobs, registry digests
and the current chart commit. It checks the SDK wheel checksum and downstream
SRM verification. The normal SRM image retains its locked SDK dependency: the
and the current chart commit. Set `candidate_ref` in the profile to `main` for
a coordinated release, or `develop` for iterative integration (the default).
All component and chart heads come from that ref; missing builds fail instead
of falling back to another branch or an older image. It checks the SDK wheel
checksum and downstream SRM verification. The normal SRM image retains its locked SDK dependency: the
runner compares its runtime contents with the wheel built from current SDK
source, and fails if they differ. A package-version difference remains visible
in `candidate.json`; matching runtime contents never imply a final version has
+1 −0
Changes for ci/lab/profile.release-2.0.json: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
{
  "candidate_ref": "main",
  "chart_revision": "52a7b0d942af88d004a0781e365b0a6db7c66e2c",
  "images": {
    "oeg": "labs.etsi.org:5050/oop/code/open-exposure-gateway:ci-develop-7434f8ae@sha256:7c06caf7ad6d6bf67bd40a17f33db79bcb9fddb2ab1f79e949a0d7750d4d5907",
+8 −4
Changes for ci/resolve_candidate.py: 8 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -38,6 +38,9 @@ def resolve_candidate(original, directory, *, fetch=download):
    The SDK head wheel is a CI artifact, not a rewritten registry version.
    A changed SDK runtime or stale downstream check requires reintegration.
    """
    candidate_ref = original.get("candidate_ref", "develop")
    if candidate_ref not in ("develop", "main"):
        raise ValueError("candidate_ref must be develop or main")
    directory = Path(directory)
    directory.mkdir(parents=True, exist_ok=True)
    profile = deepcopy(original)
@@ -48,17 +51,18 @@ def resolve_candidate(original, directory, *, fetch=download):
    projects = {key: api("projects/" + quote(path, safe=""))["id"]
                for key, path in PROJECTS.items()}

    def branch(key, ref="develop"):
    def branch(key, ref=None):
        ref = candidate_ref if ref is None else ref
        return api(f"projects/{projects[key]}/repository/branches/{quote(ref, safe='')}")["commit"]["id"]

    heads = {key: branch(key) for key in PROJECTS}
    evidence = {"ref": "develop", "source_heads": heads, "components": {}}
    evidence = {"ref": candidate_ref, "source_heads": heads, "components": {}}
    profile["images"], profile["local_images"] = {}, []
    for key in ("oeg", "fm", "srm", "sdk"):
        project, sha = projects[key], heads[key]
        query = {"sha": sha, "status": "success", "per_page": 1}
        if key != "sdk":
            query["ref"] = "develop"
            query["ref"] = candidate_ref
        pipelines = api(f"projects/{project}/pipelines?" + urlencode(query))
        if not pipelines:
            raise RuntimeError(f"Current {key} head has no successful candidate pipeline; no older fallback")
@@ -99,7 +103,7 @@ def resolve_candidate(original, directory, *, fetch=download):
        else:
            repositories = api(f"projects/{project}/registry/repositories?per_page=100")
            repository = next(row for row in repositories if row["path"] == PROJECTS[key])
            tag = "ci-develop-" + sha[:8]
            tag = "ci-" + candidate_ref + "-" + sha[:8]
            image = api(f"projects/{project}/registry/repositories/{repository['id']}/tags/{tag}")
            if not image["digest"].startswith("sha256:"):
                raise ValueError("Registry did not identify an immutable image")