Prepare TF-SDK for release 2.0.0

.Integrates the TF-SDK contract and adapter updates through 12ed672 into develop for Release 2.0.0.

Includes Kubernetes container deployment through Helm, QoD and location fixes, structured logging and explicit adapter support checks.

Local SDK verification: 120 offline tests passed; one live case was deselected. The inspected SRM suite has 260 passes and two existing termination assertions that require separate resolution. Live OEG-driven acceptance remains pending.

Before merging, agree the supported release scope, prepare a fresh package version and safe publication rules, and verify the final candidate with SDK, consumer and applicable OEG-driven checks.

Candidate verification — 21 October 2026

v2.0.0rc2 at c748ee1 passed pipeline 28807: 137 offline tests on each supported Python version, installed-package checks, Helm 3.18.6, typing and security checks. The published wheel/sdist checksums match the tested artifacts.

SRM !42 pins the published RC. Its normal pipeline passed 526 tests; exact-wheel requalification passed 262 unit/bridge tests. Ansible !5 adds isolated acceptance checkpoints with 18 offline safety checks and unchanged default selection. Live OEG-driven acceptance and final 2.0.0 qualification remain pending.

Edited by Konstantinos Togias

Merge request reports

Loading

ci/README.md

0 → 100644
+60 −0
Changes for ci/README.md: 60 added lines, 0 removed lines.
Original line number Diff line number Diff line
# SDK verification and release gates

The default pipeline validates Python 3.12 using a hashed dependency/tool lock.
Run `python ci/verify.py SUITE` for lint, build, tests, sdist, charts and typing.
Build first; subsequent suites consume that wheel, not an editable checkout.
Tests select contract, common and offline location cases explicitly. Live NEF
cases are excluded and require separate acceptance evidence.

JUnit, focused contract branch coverage (81% minimum), environment inventories,
distribution hashes and packaged chart renders are retained as CI artifacts.
Helm 3.18.6 is downloaded with its official archive checksum. Python 3.10,
3.11 and 3.13 run in scheduled and release pipelines using digest-pinned images.
Update tools.in, requirements.lock, runtime.lock and toolchain.json together.
Locks were compiled with `uv pip compile pyproject.toml ci/tools.in --universal --python-version 3.10
--generate-hashes` (omit ci/tools.in for runtime.lock). Review dependency updates.

`python ci/security.py secrets` rejects new findings. The reviewed fingerprint
file contains hashes and reasons for synthetic schema examples, generated field
maps and the Helm checksum; only this hash inventory is excluded from scanning.
Changing a value or moving it to another file requires a new review. Access-token
fixtures use an explicit example value. Never place real credentials in fixtures.
Dependency audit produces vulnerability JSON and CycloneDX SBOM; tag and nightly
runs must pass. Audit service failures cannot produce a clean result.

Source, sdist and wheel are checked before publication. Branch pushes cannot
publish packages. Release promotion must reuse the built and tested bytes;
never rebuild a package under an existing version. Final release qualification
must identify the exact SDK wheel, SRM revision and OEG-driven acceptance run.

The candidate version is 2.0.0rc3. A maintainer creates a protected matching
release tag only when the intended candidate is ready. Publication remains a
blocking manual job, uses CI_JOB_TOKEN, checks tag/version/source and artifact
checksums, and retains a permanent release receipt. No branch pipeline uploads.
Final 2.0.0 additionally needs exact-wheel SRM and live E2E qualification.

GitLab Runner downloads artifacts before before_script. Its initial TMPDIR is the
existing project directory; individual verification commands then create and use
.cache/ci/tmp. This keeps temporary files off tmpfs without breaking downloads.

Python support is bounded to >=3.10,<3.14, matching the classifiers and tested
matrix; the current pinned pydantic-core cannot build with Python 3.14. Release
qualification files are maintainer-reviewed receipts, not signed attestations or
a claimed SLSA level. Each SRM/E2E receipt must carry the same SDK SHA/checksum
and a pipeline URL in its owning ETSI project. Keep final publication blocked
until the release scope and the corresponding live evidence are agreed.

Consumer qualification follows a protected SRM candidate branch. GitLab fixes
the downstream checkout when creating that pipeline, and its receipt records
the exact consumer revision. Advancing SRM can requalify the same immutable SDK
artifact without changing the package or inventing a new SDK version. Use
RUN_EXTENDED=1 to run the full Python/security matrix in a web pipeline too.

The downstream trigger disables inheritance of SDK default variables and passes
only the explicit SDK artifact coordinates. Job-only paths such as TMPDIR must
be established by the owning project. Candidate rc1 exposed this boundary issue
and was not published; rc2 retains a new immutable tag instead of moving rc1.

Packaged container chart checks render canonical CPU millicores and decimal MB
requests, including omission and absence of invented limits. Provider regressions
exercise the same mapping through activation and update.

ci/release.py

0 → 100644
+116 −0
Changes for ci/release.py: 116 added lines, 0 removed lines.
Original line number Diff line number Diff line
"""Check release identity and qualification before publishing verified artifacts."""

import argparse
import json
import os
import re
import subprocess
import sys
import zipfile
from email.parser import BytesParser
from pathlib import Path

from packaging.version import Version
from verify import DIST, OUT, ROOT, sha256, write_json


def check_tag(tag, version, protected):
    if not re.fullmatch(r"v2\.0\.\d+(?:rc[1-9][0-9]*)?", tag):
        raise ValueError("Use an immutable v2.0.X or v2.0.XrcN release tag")
    if Version(tag[1:]) != Version(version):
        raise ValueError("Release tag and package version differ")
    if protected != "true":
        raise ValueError("Publication requires a protected release tag")


def check_evidence(evidence, manifest, wheel_hash):
    if evidence.get("sdk_sha") != manifest["source_sha"]:
        raise ValueError("Qualification belongs to another SDK source revision")
    if evidence.get("wheel_sha256") != wheel_hash:
        raise ValueError("Qualification belongs to another wheel")
    for component in ("srm", "e2e"):
        item = evidence.get(component, {})
        if item.get("status") != "passed" or not item.get("pipeline_url"):
            raise ValueError(f"Successful {component} qualification is required")
        expected_project = "service-resource-manager" if component == "srm" else "ansible"
        group = "code" if component == "srm" else "deploy"
        prefix = f"https://labs.etsi.org/rep/oop/{group}/{expected_project}/-/pipelines/"
        if not re.fullmatch(re.escape(prefix) + r"[0-9]+", item["pipeline_url"]):
            raise ValueError(f"Unexpected {component} qualification pipeline")
        if item.get("sdk_sha") != manifest["source_sha"] or item.get("wheel_sha256") != wheel_hash:
            raise ValueError(f"The {component} receipt belongs to another SDK artifact")
        if not re.fullmatch(r"[0-9a-f]{40}", item.get("source_sha", "")):
            raise ValueError(f"Missing exact {component} source revision")


def verify_release():
    manifest = json.loads((OUT / "manifest.json").read_text())
    for item in manifest["artifacts"]:
        if sha256(DIST / item["name"]) != item["sha256"]:
            raise ValueError("Artifact changed after verification")
    wheel = next(DIST.glob("*.whl"))
    with zipfile.ZipFile(wheel) as archive:
        name = next(n for n in archive.namelist() if n.endswith(".dist-info/METADATA"))
        metadata = BytesParser().parsebytes(archive.read(name))
    if metadata["Name"] != "openop-tfsdk":
        raise ValueError("Unexpected distribution name")
    version = metadata["Version"]
    check_tag(os.getenv("CI_COMMIT_TAG", ""), version, os.getenv("CI_COMMIT_REF_PROTECTED"))
    if manifest["source_sha"] != os.environ["CI_COMMIT_SHA"]:
        raise ValueError("Build and tag source revisions differ")
    # Tag pipelines may fetch only a detached tag; refresh the allowed ancestry refs.
    subprocess.run(["git", "fetch", "origin", "+refs/heads/*:refs/remotes/origin/*"], check=True)
    branches = ["origin/develop", "origin/main", "origin/refactor/tfsdk-architecture-alignment"]
    if not any(
        subprocess.run(
            ["git", "merge-base", "--is-ancestor", "HEAD", ref], capture_output=True
        ).returncode
        == 0
        for ref in branches
    ):
        raise ValueError("Tagged commit is outside the reviewed integration lines")
    if not Version(version).is_prerelease:
        path = os.getenv("RELEASE_QUALIFICATION_FILE")
        if not path:
            raise ValueError("Final publication requires the reviewed SRM/E2E qualification file")
        check_evidence(json.loads(Path(path).read_text()), manifest, sha256(wheel))
    write_json(
        OUT / "release-check.json",
        {
            "tag": os.environ["CI_COMMIT_TAG"],
            "version": version,
            "source_sha": manifest["source_sha"],
            "wheel_sha256": sha256(wheel),
            "qualification": "candidate" if Version(version).is_prerelease else "final",
        },
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=["check", "publish"])
    args = parser.parse_args()
    os.chdir(ROOT)
    verify_release()
    if args.action == "publish":
        environment = dict(
            os.environ, TWINE_USERNAME="gitlab-ci-token", TWINE_PASSWORD=os.environ["CI_JOB_TOKEN"]
        )
        url = f"{os.environ['CI_API_V4_URL']}/projects/{os.environ['CI_PROJECT_ID']}/packages/pypi"
        subprocess.run(
            [
                sys.executable,
                "-m",
                "twine",
                "upload",
                "--repository-url",
                url,
                *[str(p) for p in DIST.iterdir()],
            ],
            env=environment,
            check=True,
        )


if __name__ == "__main__":
    main()

ci/requirements.lock

0 → 100644
+1567 −0

File added.

Preview size limit exceeded, changes collapsed.

ci/runtime.lock

0 → 100644
+716 −0

File added.

Preview size limit exceeded, changes collapsed.

+107 −0
Changes for ci/secrets-reviewed.json: 107 added lines, 0 removed lines.
Original line number Diff line number Diff line
{
  "schema_version": 1,
  "entries": [
    {
      "path": "ci/toolchain.json",
      "type": "Hex High Entropy String",
      "hashed_secret": "30a6c9c8622c6d986b08de0a2fc366f404598c2d",
      "reason": "SHA-256 of the official Helm archive."
    },
    {
      "path": "src/openop_tfsdk/edgecloud/adapters/kubernetes/lib/models/artifact_exists_model.py",
      "type": "Secret Keyword",
      "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
      "reason": "Generated model attribute mapping; the string is a field name."
    },
    {
      "path": "src/openop_tfsdk/edgecloud/adapters/kubernetes/lib/models/copy_artifact_model.py",
      "type": "Secret Keyword",
      "hashed_secret": "64df0c6cc24b853f4e226cd6587f757ddb3a1854",
      "reason": "Generated model attribute mapping; the string is a field name."
    },
    {
      "path": "src/openop_tfsdk/edgecloud/adapters/kubernetes/lib/models/copy_artifact_model.py",
      "type": "Secret Keyword",
      "hashed_secret": "a46adb92e1d018f8a43a3c25275bdac4033c3ccb",
      "reason": "Generated model attribute mapping; the string is a field name."
    },
    {
      "path": "src/openop_tfsdk/edgecloud/adapters/kubernetes/lib/models/helm_install_model.py",
      "type": "Secret Keyword",
      "hashed_secret": "f60ad14ccc92aef07f54c08d901ee06d50868722",
      "reason": "Generated model attribute mapping; the string is a field name."
    },
    {
      "path": "tests/edgecloud/test_config_camara.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "4053e05aab61c4ec888ea1401c8fd48ec37fa599",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "435983de815d0f09f68e470445ea27157d0060d2",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "c9ef915968d7a5f8f8552c0d391acbe553392ee4",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "28fe94c341848558dd74f214970f9d50e1be07d4",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Hex High Entropy String",
      "hashed_secret": "95adc1f2652584e3bd1ffe28b89baf041b4b9934",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "74ed59406a507d3b45e5b94924cdd38e811d256b",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "60158f4a292e973eb5247a702cc2ca1ad3dc898e",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "669d42b05fc55db4ed5aa01844e97d831b9cf709",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "ee80f406e64f8f715622ddb68e1f240f943f32e5",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "b3b60ed1b8a268253df573f7c34353e5b979e74e",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Base64 High Entropy String",
      "hashed_secret": "e33a7b57c840f59abea0a3e21fafcc2da216151a",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    },
    {
      "path": "tests/edgecloud/test_config_gsma.py",
      "type": "Secret Keyword",
      "hashed_secret": "ecb252044b5ea0f679ee78ec1a12904739e2904d",
      "reason": "Synthetic schema-example identifier, or literal string placeholder; no authentication value."
    }
  ]
}
Loading
Loading