Loading .gitlab-ci.yml +54 −0 Changes for .gitlab-ci.yml: 54 added lines, 0 removed lines. Original line number Diff line number Diff line workflow: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_OPEN_MERGE_REQUESTS' when: never - when: always default: image: python:3.12-slim cache: Loading @@ -22,26 +30,46 @@ variables: UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv" type: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: type script: - mypy src/ tests/ architecture: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: architecture script: - lint-imports lint: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: lint script: - ruff check src/ tests/ format: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: format script: - ruff format --check src/ tests/ test: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: test variables: DOCKER_HOST: tcp://docker:2375 Loading @@ -68,4 +96,30 @@ build-and-push: - docker buildx build --provenance=false --network=host -t "$CI_REGISTRY_IMAGE:$TEST_IMAGE_TAG" --push . - docker logout "$CI_REGISTRY" rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"' sdk-consumer: stage: test image: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 timeout: 15 minutes variables: TMPDIR: "$CI_PROJECT_DIR/.cache/sdk-tmp" PYTHONDONTWRITEBYTECODE: "1" rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' - when: never before_script: - mkdir -p "$TMPDIR" - pip install uv==0.11.26 - uv sync --locked --extra dev --no-install-project script: - python ci/sdk_consumer.py artifacts: when: always paths: - .cache/sdk-consumer/qualification.json reports: junit: .cache/sdk-consumer/junit.xml expire_in: 90 days ci/sdk-consumer.md 0 → 100644 +20 −0 Changes for ci/sdk-consumer.md: 20 added lines, 0 removed lines. Original line number Diff line number Diff line # SDK candidate qualification The `sdk-consumer` job accepts candidates only in a downstream pipeline from TF-SDK project 396. CI_JOB_TOKEN downloads a single build job's artifact; source SHA, manifest membership and wheel SHA-256 must match the request. The normal locked environment is prepared without installing SRM's distribution; the candidate wheel replaces the baseline SDK and installs its declared runtime dependencies. `uv pip check` must pass. Source imports use SRM's src directory. The lock remains the published 1.1.1 baseline until a qualified 2.0 package exists. All tests/unit tests run, including the real SDK factory/bridge boundary. Missing configuration, skipped tests and failures block qualification. The receipt records both source revisions, the wheel checksum and CI pipeline URL. These offline checks cannot establish cluster, NEF or OEG-driven acceptance. Other SRM pipeline jobs retain their normal behavior outside this explicit consumer mode. Local execution accepts SDK_WHEEL_FILE and SDK_MANIFEST_FILE instead of a remote job; also supply SDK_SOURCE_SHA, SDK_WHEEL_NAME and SDK_WHEEL_SHA256. Prepare `.venv` with `uv sync --locked --extra dev --no-install-project`, then run `python ci/sdk_consumer.py` using a Python 3.12 environment and task TMPDIR. ci/sdk_consumer.py 0 → 100644 +95 −0 Changes for ci/sdk_consumer.py: 95 added lines, 0 removed lines. Original line number Diff line number Diff line """Qualify the actual SDK wheel against SRM's offline consumer suite.""" import hashlib import json import os import re import subprocess import zipfile from pathlib import Path from urllib.request import Request, urlopen from xml.etree import ElementTree ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / ".cache/sdk-consumer" def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def main(): OUT.mkdir(parents=True, exist_ok=True) expected_srm = os.getenv("EXPECTED_SRM_SHA") revision = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() if expected_srm and revision != expected_srm: raise ValueError("Consumer checkout differs from the requested SRM revision") sha = os.environ["SDK_SOURCE_SHA"] expected_hash = os.environ["SDK_WHEEL_SHA256"] wheel_name = os.environ["SDK_WHEEL_NAME"] if not re.fullmatch(r"[a-f0-9]{40}", sha) or not re.fullmatch(r"[a-f0-9]{64}", expected_hash): raise ValueError("Exact SDK source revision and wheel checksum are required") if Path(wheel_name).name != wheel_name or not wheel_name.endswith(".whl"): raise ValueError("Invalid wheel name") local = os.getenv("SDK_WHEEL_FILE") if local: wheel = Path(local).resolve() manifest = json.loads(Path(os.environ["SDK_MANIFEST_FILE"]).read_text()) else: job = os.environ["SDK_BUILD_JOB_ID"] if not job.isdecimal() or os.environ["SDK_PROJECT_ID"] != "396": raise ValueError("Only a TF-SDK build job can supply the candidate") url = f"{os.environ['CI_API_V4_URL']}/projects/396/jobs/{job}/artifacts" request = Request(url, headers={"JOB-TOKEN": os.environ["CI_JOB_TOKEN"]}) archive = OUT / "sdk-build.zip" with urlopen(request, timeout=90) as response: archive.write_bytes(response.read()) with zipfile.ZipFile(archive) as contents: manifest = json.loads(contents.read(".cache/ci/manifest.json")) wheel = OUT / wheel_name wheel.write_bytes(contents.read(f".cache/ci/dist/{wheel_name}")) if manifest["source_sha"] != sha or digest(wheel) != expected_hash: raise ValueError("SDK artifact identity differs from the requested candidate") if not any(x == {"name": wheel_name, "sha256": expected_hash} for x in manifest["artifacts"]): raise ValueError("Wheel is absent from the build manifest") python = ROOT / ".venv/bin/python" subprocess.run(["uv", "pip", "install", "--python", str(python), str(wheel)], check=True) subprocess.run(["uv", "pip", "check", "--python", str(python)], check=True) environment = dict(os.environ, PYTHONPATH=str(ROOT / "src")) report = OUT / "junit.xml" subprocess.run( [ str(python), "-m", "pytest", "--confcutdir=tests/unit", "tests/unit", f"--junitxml={report}", ], env=environment, check=True, ) suites = ElementTree.parse(report).getroot() if any(int(s.get("skipped", "0")) for s in suites.iter("testsuite")): raise ValueError("Skipped consumer tests cannot qualify the candidate") revision = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() (OUT / "qualification.json").write_text( json.dumps( { "status": "passed", "source_sha": revision, "pipeline_url": os.getenv("CI_PIPELINE_URL"), "sdk_sha": sha, "wheel_sha256": expected_hash, "build_job_id": manifest.get("build_job_id"), "baseline_lock_overlay": True, "scope": "offline consumer unit and real SDK bridge tests; no live E2E", }, indent=2, ) + "\n" ) if __name__ == "__main__": main() Loading
.gitlab-ci.yml +54 −0 Changes for .gitlab-ci.yml: 54 added lines, 0 removed lines. Original line number Diff line number Diff line workflow: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_OPEN_MERGE_REQUESTS' when: never - when: always default: image: python:3.12-slim cache: Loading @@ -22,26 +30,46 @@ variables: UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv" type: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: type script: - mypy src/ tests/ architecture: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: architecture script: - lint-imports lint: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: lint script: - ruff check src/ tests/ format: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: format script: - ruff format --check src/ tests/ test: rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - when: on_success stage: test variables: DOCKER_HOST: tcp://docker:2375 Loading @@ -68,4 +96,30 @@ build-and-push: - docker buildx build --provenance=false --network=host -t "$CI_REGISTRY_IMAGE:$TEST_IMAGE_TAG" --push . - docker logout "$CI_REGISTRY" rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"' sdk-consumer: stage: test image: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 timeout: 15 minutes variables: TMPDIR: "$CI_PROJECT_DIR/.cache/sdk-tmp" PYTHONDONTWRITEBYTECODE: "1" rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' - when: never before_script: - mkdir -p "$TMPDIR" - pip install uv==0.11.26 - uv sync --locked --extra dev --no-install-project script: - python ci/sdk_consumer.py artifacts: when: always paths: - .cache/sdk-consumer/qualification.json reports: junit: .cache/sdk-consumer/junit.xml expire_in: 90 days
ci/sdk-consumer.md 0 → 100644 +20 −0 Changes for ci/sdk-consumer.md: 20 added lines, 0 removed lines. Original line number Diff line number Diff line # SDK candidate qualification The `sdk-consumer` job accepts candidates only in a downstream pipeline from TF-SDK project 396. CI_JOB_TOKEN downloads a single build job's artifact; source SHA, manifest membership and wheel SHA-256 must match the request. The normal locked environment is prepared without installing SRM's distribution; the candidate wheel replaces the baseline SDK and installs its declared runtime dependencies. `uv pip check` must pass. Source imports use SRM's src directory. The lock remains the published 1.1.1 baseline until a qualified 2.0 package exists. All tests/unit tests run, including the real SDK factory/bridge boundary. Missing configuration, skipped tests and failures block qualification. The receipt records both source revisions, the wheel checksum and CI pipeline URL. These offline checks cannot establish cluster, NEF or OEG-driven acceptance. Other SRM pipeline jobs retain their normal behavior outside this explicit consumer mode. Local execution accepts SDK_WHEEL_FILE and SDK_MANIFEST_FILE instead of a remote job; also supply SDK_SOURCE_SHA, SDK_WHEEL_NAME and SDK_WHEEL_SHA256. Prepare `.venv` with `uv sync --locked --extra dev --no-install-project`, then run `python ci/sdk_consumer.py` using a Python 3.12 environment and task TMPDIR.
ci/sdk_consumer.py 0 → 100644 +95 −0 Changes for ci/sdk_consumer.py: 95 added lines, 0 removed lines. Original line number Diff line number Diff line """Qualify the actual SDK wheel against SRM's offline consumer suite.""" import hashlib import json import os import re import subprocess import zipfile from pathlib import Path from urllib.request import Request, urlopen from xml.etree import ElementTree ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / ".cache/sdk-consumer" def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def main(): OUT.mkdir(parents=True, exist_ok=True) expected_srm = os.getenv("EXPECTED_SRM_SHA") revision = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() if expected_srm and revision != expected_srm: raise ValueError("Consumer checkout differs from the requested SRM revision") sha = os.environ["SDK_SOURCE_SHA"] expected_hash = os.environ["SDK_WHEEL_SHA256"] wheel_name = os.environ["SDK_WHEEL_NAME"] if not re.fullmatch(r"[a-f0-9]{40}", sha) or not re.fullmatch(r"[a-f0-9]{64}", expected_hash): raise ValueError("Exact SDK source revision and wheel checksum are required") if Path(wheel_name).name != wheel_name or not wheel_name.endswith(".whl"): raise ValueError("Invalid wheel name") local = os.getenv("SDK_WHEEL_FILE") if local: wheel = Path(local).resolve() manifest = json.loads(Path(os.environ["SDK_MANIFEST_FILE"]).read_text()) else: job = os.environ["SDK_BUILD_JOB_ID"] if not job.isdecimal() or os.environ["SDK_PROJECT_ID"] != "396": raise ValueError("Only a TF-SDK build job can supply the candidate") url = f"{os.environ['CI_API_V4_URL']}/projects/396/jobs/{job}/artifacts" request = Request(url, headers={"JOB-TOKEN": os.environ["CI_JOB_TOKEN"]}) archive = OUT / "sdk-build.zip" with urlopen(request, timeout=90) as response: archive.write_bytes(response.read()) with zipfile.ZipFile(archive) as contents: manifest = json.loads(contents.read(".cache/ci/manifest.json")) wheel = OUT / wheel_name wheel.write_bytes(contents.read(f".cache/ci/dist/{wheel_name}")) if manifest["source_sha"] != sha or digest(wheel) != expected_hash: raise ValueError("SDK artifact identity differs from the requested candidate") if not any(x == {"name": wheel_name, "sha256": expected_hash} for x in manifest["artifacts"]): raise ValueError("Wheel is absent from the build manifest") python = ROOT / ".venv/bin/python" subprocess.run(["uv", "pip", "install", "--python", str(python), str(wheel)], check=True) subprocess.run(["uv", "pip", "check", "--python", str(python)], check=True) environment = dict(os.environ, PYTHONPATH=str(ROOT / "src")) report = OUT / "junit.xml" subprocess.run( [ str(python), "-m", "pytest", "--confcutdir=tests/unit", "tests/unit", f"--junitxml={report}", ], env=environment, check=True, ) suites = ElementTree.parse(report).getroot() if any(int(s.get("skipped", "0")) for s in suites.iter("testsuite")): raise ValueError("Skipped consumer tests cannot qualify the candidate") revision = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() (OUT / "qualification.json").write_text( json.dumps( { "status": "passed", "source_sha": revision, "pipeline_url": os.getenv("CI_PIPELINE_URL"), "sdk_sha": sha, "wheel_sha256": expected_hash, "build_job_id": manifest.get("build_job_id"), "baseline_lock_overlay": True, "scope": "offline consumer unit and real SDK bridge tests; no live E2E", }, indent=2, ) + "\n" ) if __name__ == "__main__": main()