Loading .gitignore +3 −0 Changes for .gitignore: 3 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -8,3 +8,6 @@ __pycache__/ .tar.gz /dist src/openop_tfsdk.egg-info/ .cache/ .coverage .gitlab-ci.yml +143 −52 Changes for .gitlab-ci.yml: 143 added lines, 52 removed lines. Original line number Diff line number Diff line workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_TAG =~ /^v2\.0\.[0-9]+(rc[1-9][0-9]*)?$/ - if: $CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH - if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web" - when: never stages: - validate - build - package - consumer - qualify - publish default: image: python:3.12-slim image: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 interruptible: true timeout: 15 minutes before_script: - mkdir -p "$TMPDIR" "$PIP_CACHE_DIR" - python --version - python -m pip install --require-hashes -r ci/requirements.lock cache: key: files: - ci/requirements.lock prefix: pip-$PYTHON_VERSION paths: - .cache/pip/ variables: PACKAGE_NAME: openop-tfsdk validate-mr: PYTHON_VERSION: '3.12' PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip TMPDIR: $CI_PROJECT_DIR/.cache/ci/tmp PYTHONDONTWRITEBYTECODE: '1' PIP_DISABLE_PIP_VERSION_CHECK: '1' GIT_DEPTH: '0' lint: stage: validate before_script: - echo "Running merge request validation..." - pip install -r requirements.txt - pip install -e . - pip install isort black flake8 pytest script: - echo "Running linters..." - isort src tests --check --profile black --filter-files - black src tests --check --line-length=100 - flake8 src tests - echo "Running tests..." - pytest -v tests/common/test_invoke_edgecloud_clients.py - pytest -v tests/common/test_invoke_network_clients.py - pytest -v tests/contract/ only: - merge_requests - python ci/verify.py lint secret-scan: stage: validate script: - python ci/security.py secrets artifacts: when: always paths: - .cache/ci/secrets.json expire_in: 14 days build-package: stage: build before_script: - pip install build twine - pip install -r requirements.txt - pip install -e . script: - echo "Building Python package..." - python -m build - echo "Package built successfully" - ls -la dist/ - python ci/verify.py build artifacts: paths: - dist/ expire_in: 1 day only: - main - develop - tags publish-gitlab: stage: publish dependencies: - build-package before_script: - pip install twine - .cache/ci/dist/ - .cache/ci/manifest.json - .cache/ci/build.env reports: dotenv: .cache/ci/build.env expire_in: 90 days wheel-tests: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py tests artifacts: when: always paths: - .cache/ci/junit.xml - .cache/ci/coverage.xml - .cache/ci/coverage.json - .cache/ci/environment.json reports: junit: .cache/ci/junit.xml coverage_report: coverage_format: cobertura path: .cache/ci/coverage.xml expire_in: 90 days coverage: /TOTAL.*\s([0-9]+(?:\.[0-9]+)?%)$/ sdist-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py sdist chart-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py charts artifacts: when: always paths: - .cache/ci/chart-*.yaml expire_in: 14 days public-contract-types: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py typing dependency-audit: stage: package script: - echo "Validating package format..." - twine check dist/* - echo "Publishing to Package Registry..." - TWINE_PASSWORD="$CI_JOB_TOKEN" TWINE_USERNAME="gitlab-ci-token" python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* - echo "Package published successfully" only: - main - develop - tags - python ci/security.py dependencies rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" - when: manual allow_failure: true artifacts: when: always paths: - .cache/ci/vulnerabilities.json - .cache/ci/security-summary.json - .cache/ci/sbom.json expire_in: 90 days python-compatibility: stage: package image: $PYTHON_IMAGE parallel: matrix: - PYTHON_VERSION: - '3.10' PYTHON_IMAGE: - python:3.10-slim@sha256:6ff506466f8b1e981719b468cc0023b8ef000b983cdec0eafe119d01f379e31d - PYTHON_VERSION: - '3.11' PYTHON_IMAGE: - python:3.11-slim@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce - PYTHON_VERSION: - '3.13' PYTHON_IMAGE: - python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f needs: - job: build-package artifacts: true rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" script: - python ci/verify.py tests artifacts: when: always reports: junit: .cache/ci/junit.xml paths: - .cache/ci/environment.json expire_in: 90 days .pre-commit-config.yaml +2 −2 Changes for .pre-commit-config.yaml: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ repos: name: isort (python) args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 25.1.0 rev: 24.8.0 hooks: - id: black args: Loading @@ -21,7 +21,7 @@ repos: - "--target-version=py310" - "--line-length=100" - repo: https://github.com/pycqa/flake8 rev: 7.2.0 rev: 7.1.1 hooks: - id: flake8 additional_dependencies: [] ci/README.md 0 → 100644 +28 −0 Changes for ci/README.md: 28 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 --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. ci/release.py 0 → 100644 +107 −0 Changes for ci/release.py: 107 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") 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") 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() Loading
.gitignore +3 −0 Changes for .gitignore: 3 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -8,3 +8,6 @@ __pycache__/ .tar.gz /dist src/openop_tfsdk.egg-info/ .cache/ .coverage
.gitlab-ci.yml +143 −52 Changes for .gitlab-ci.yml: 143 added lines, 52 removed lines. Original line number Diff line number Diff line workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_TAG =~ /^v2\.0\.[0-9]+(rc[1-9][0-9]*)?$/ - if: $CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH - if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web" - when: never stages: - validate - build - package - consumer - qualify - publish default: image: python:3.12-slim image: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 interruptible: true timeout: 15 minutes before_script: - mkdir -p "$TMPDIR" "$PIP_CACHE_DIR" - python --version - python -m pip install --require-hashes -r ci/requirements.lock cache: key: files: - ci/requirements.lock prefix: pip-$PYTHON_VERSION paths: - .cache/pip/ variables: PACKAGE_NAME: openop-tfsdk validate-mr: PYTHON_VERSION: '3.12' PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip TMPDIR: $CI_PROJECT_DIR/.cache/ci/tmp PYTHONDONTWRITEBYTECODE: '1' PIP_DISABLE_PIP_VERSION_CHECK: '1' GIT_DEPTH: '0' lint: stage: validate before_script: - echo "Running merge request validation..." - pip install -r requirements.txt - pip install -e . - pip install isort black flake8 pytest script: - echo "Running linters..." - isort src tests --check --profile black --filter-files - black src tests --check --line-length=100 - flake8 src tests - echo "Running tests..." - pytest -v tests/common/test_invoke_edgecloud_clients.py - pytest -v tests/common/test_invoke_network_clients.py - pytest -v tests/contract/ only: - merge_requests - python ci/verify.py lint secret-scan: stage: validate script: - python ci/security.py secrets artifacts: when: always paths: - .cache/ci/secrets.json expire_in: 14 days build-package: stage: build before_script: - pip install build twine - pip install -r requirements.txt - pip install -e . script: - echo "Building Python package..." - python -m build - echo "Package built successfully" - ls -la dist/ - python ci/verify.py build artifacts: paths: - dist/ expire_in: 1 day only: - main - develop - tags publish-gitlab: stage: publish dependencies: - build-package before_script: - pip install twine - .cache/ci/dist/ - .cache/ci/manifest.json - .cache/ci/build.env reports: dotenv: .cache/ci/build.env expire_in: 90 days wheel-tests: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py tests artifacts: when: always paths: - .cache/ci/junit.xml - .cache/ci/coverage.xml - .cache/ci/coverage.json - .cache/ci/environment.json reports: junit: .cache/ci/junit.xml coverage_report: coverage_format: cobertura path: .cache/ci/coverage.xml expire_in: 90 days coverage: /TOTAL.*\s([0-9]+(?:\.[0-9]+)?%)$/ sdist-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py sdist chart-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py charts artifacts: when: always paths: - .cache/ci/chart-*.yaml expire_in: 14 days public-contract-types: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py typing dependency-audit: stage: package script: - echo "Validating package format..." - twine check dist/* - echo "Publishing to Package Registry..." - TWINE_PASSWORD="$CI_JOB_TOKEN" TWINE_USERNAME="gitlab-ci-token" python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* - echo "Package published successfully" only: - main - develop - tags - python ci/security.py dependencies rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" - when: manual allow_failure: true artifacts: when: always paths: - .cache/ci/vulnerabilities.json - .cache/ci/security-summary.json - .cache/ci/sbom.json expire_in: 90 days python-compatibility: stage: package image: $PYTHON_IMAGE parallel: matrix: - PYTHON_VERSION: - '3.10' PYTHON_IMAGE: - python:3.10-slim@sha256:6ff506466f8b1e981719b468cc0023b8ef000b983cdec0eafe119d01f379e31d - PYTHON_VERSION: - '3.11' PYTHON_IMAGE: - python:3.11-slim@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce - PYTHON_VERSION: - '3.13' PYTHON_IMAGE: - python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f needs: - job: build-package artifacts: true rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" script: - python ci/verify.py tests artifacts: when: always reports: junit: .cache/ci/junit.xml paths: - .cache/ci/environment.json expire_in: 90 days
.pre-commit-config.yaml +2 −2 Changes for .pre-commit-config.yaml: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ repos: name: isort (python) args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 25.1.0 rev: 24.8.0 hooks: - id: black args: Loading @@ -21,7 +21,7 @@ repos: - "--target-version=py310" - "--line-length=100" - repo: https://github.com/pycqa/flake8 rev: 7.2.0 rev: 7.1.1 hooks: - id: flake8 additional_dependencies: []
ci/README.md 0 → 100644 +28 −0 Changes for ci/README.md: 28 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 --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.
ci/release.py 0 → 100644 +107 −0 Changes for ci/release.py: 107 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") 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") 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()