Commit 5dad459a authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

ci: accept the exact locked SDK after stable index adoption

parent eadd1ff2
Loading
Loading
Loading
Loading
+5 −0
Changes for ci/sdk-consumer.md: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -12,6 +12,11 @@ requirements and its content-addressed URL under `[tool.uv.sources]`. Run
The lock must select that version, URL and wheel checksum. A dependency change
is therefore visible in the SRM MR before any qualification starts.

After stable SDK publication, the SRM MR may replace the candidate URL with the
normal explicit SDK PyPI index and commit its regenerated lock. Qualification
also accepts that form when the declared version and locked wheel hash match
the exact SDK under test. A different wheel or index remains an error.

The `sdk-consumer` job downloads the checked artifact and copies the committed
`pyproject.toml` and `uv.lock` unchanged into `.cache/sdk-consumer/project`.
It rejects an undeclared candidate before installation, then uses
+24 −10
Changes for ci/sdk_consumer.py: 24 added lines, 10 removed lines.
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ REGISTRY = "labs.etsi.org:5050/oop/code/service-resource-manager"
CANDIDATE_REGISTRY = (
    "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates"
)
SDK_INDEX = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple"


def digest(path):
@@ -126,12 +127,24 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url
    require(len(packages) == 1, "The committed lock must select exactly one SDK")
    package = packages[0]
    expected_url = f"{CANDIDATE_REGISTRY}/{sdk['sha256']}/{sdk['filename']}"
    declared_url = (
        declaration.get("tool", {})
        .get("uv", {})
        .get("sources", {})
        .get("openop-tfsdk", {})
        .get("url")
    uv = declaration.get("tool", {}).get("uv", {})
    declared_source = uv.get("sources", {}).get("openop-tfsdk", {})
    declared_url = declared_source.get("url")
    locked_wheels = [
        p for p in package.get("wheels", []) if p.get("hash") == "sha256:" + sdk["sha256"]
    ]
    candidate_pin = (
        declared_url == expected_url and package.get("source", {}).get("url") == expected_url
    )
    stable_pin = (
        package.get("source", {}).get("registry") == SDK_INDEX
        and any(
            i.get("name") == declared_source.get("index")
            and i.get("url") == SDK_INDEX
            and i.get("explicit") is True
            for i in uv.get("index", [])
        )
        and any(isinstance(p.get("url"), str) for p in locked_wheels)
    )
    require(
        package.get("version") == sdk["version"]
@@ -141,9 +154,8 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url
            )
            for item in declaration.get("project", {}).get("dependencies", [])
        )
        and declared_url == expected_url
        and package.get("source", {}).get("url") == expected_url
        and any(p.get("hash") == "sha256:" + sdk["sha256"] for p in package.get("wheels", [])),
        and (candidate_pin or stable_pin)
        and locked_wheels,
        "SDK candidate is not pinned by the committed SRM dependency and lock; "
        "update them in an SRM MR before qualification",
    )
@@ -179,7 +191,8 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url
    )
    proof["dependency_changes"] = []
    proof["source_inputs_unchanged"] = True
    proof["sdk_dependency_url"] = expected_url
    proof["sdk_dependency_kind"] = "candidate-url" if candidate_pin else "pypi"
    proof["sdk_dependency_url"] = expected_url if candidate_pin else locked_wheels[0]["url"]
    write(out / "candidate.json", proof)
    return proof

@@ -381,6 +394,7 @@ def build_image(root=ROOT, out=OUT, call=subprocess.run):
        "dependency_changes": proof["dependency_changes"],
        "source_inputs_unchanged": proof["source_inputs_unchanged"],
        "sdk_dependency_url": proof["sdk_dependency_url"],
        "sdk_dependency_kind": proof["sdk_dependency_kind"],
        "project_sha256": proof["project_sha256"],
        "runtime_dependencies": inventory["dependencies"],
        "consumer_checks": {"job_id": proof["qualification_job_id"], "status": "passed"},
+38 −0
Changes for ci/tests/test_sdk_consumer.py: 38 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -187,3 +187,41 @@ def test_install_cannot_rewrite_the_copied_lock(wheel, tmp_path, monkeypatch):

    with pytest.raises(ValueError, match="changed committed"):
        consumer.prepare(root, root / "out", env, call=changing_install)


@pytest.mark.parametrize("fault", [None, "wheel", "index"])
def test_stable_index_adoption_accepts_only_the_same_locked_wheel(
    wheel, tmp_path, monkeypatch, fault
):
    root = tmp_path / "srm"
    root.mkdir()
    env = committed_project(root, wheel)
    index = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple"
    (root / "pyproject.toml").write_text(
        '[project]\ndependencies = ["openop-tfsdk==2.0.0"]\n'
        '[tool.uv.sources]\nopenop-tfsdk = {index = "etsi-tf-sdk"}\n'
        '[[tool.uv.index]]\nname = "etsi-tf-sdk"\nurl = "' + index + '"\nexplicit = true\n'
    )
    (root / "uv.lock").write_text(
        '[[package]]\nname = "openop-tfsdk"\nversion = "2.0.0"\n'
        'source = {registry = "' + index + '"}\n'
        'wheels = [{url = "https://labs.etsi.org/rep/api/v4/projects/396/'
        'packages/pypi/files/example/candidate.whl", '
        'hash = "sha256:' + env["SDK_WHEEL_SHA256"] + '"}]\n'
    )
    monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40)
    if fault == "wheel":
        path = root / "uv.lock"
        path.write_text(path.read_text().replace(env["SDK_WHEEL_SHA256"], "f" * 64))
    if fault == "index":
        path = root / "pyproject.toml"
        path.write_text(path.read_text().replace(index, "https://example.invalid/simple"))
    if fault:
        with pytest.raises(ValueError, match="not pinned"):
            consumer.prepare(
                root, root / "out", env, call=lambda *_a, **_kw: pytest.fail("No installation")
            )
        return
    proof = consumer.prepare(root, root / "out", env, call=lambda *_a, **_kw: None)
    assert proof["source_inputs_unchanged"] is True
    assert proof["sdk_dependency_kind"] == "pypi"