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

ci: publish protected SDK release tags automatically

parent 67e4dc07
Loading
Loading
Loading
Loading
Loading
+1 −1
Changes for .gitlab-ci.yml: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -173,7 +173,7 @@ publish-package:
    - job: srm-consumer
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v2[.]0[.][0-9]+(rc[1-9][0-9]*)?$/ && $CI_COMMIT_REF_PROTECTED == "true"'
      when: manual
      when: on_success
      allow_failure: false
    - when: never
  script:
+7 −6
Changes for ci/README.md: 7 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -26,11 +26,12 @@ Source, sdist and wheel are checked before publication. Branch pushes cannot
publish packages. Release promotion reuses the built and tested bytes; never
rebuild a package under an existing version.

The candidate version is 2.0.0rc5. Create a protected matching release tag when
the intended candidate is ready. After required CI jobs pass, a maintainer plays
publish-package: this single action approves publication. No qualification file,
extra pipeline IDs or exception variables are required. CI_JOB_TOKEN uploads the
verified artifacts; tag/version/source and checksum guards remain mandatory.
The candidate version is 2.0.0rc5. Review the release scope and applicable E2E
evidence, then create a protected matching release tag when publication is
approved. After required CI jobs pass, publish-package automatically uploads
the verified artifacts using CI_JOB_TOKEN. No manual job, qualification file,
extra pipeline IDs or exception variables are required. Tag/version/source
and checksum guards remain mandatory.
An automatic informational release-qualification.json preserves actual SDK and
linked downstream test reports. Missing reports remain visible; applicable E2E
results are reviewed in the release MR. See [the release procedure](RELEASE.md).
@@ -43,7 +44,7 @@ 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
reports record available evidence, not signed attestations or a claimed SLSA
level. The release owner judges whether the tested scope is sufficient for the
release; the manual publisher records that decision in GitLab.
release before creating its protected publication tag.

Consumer qualification follows a protected SRM candidate branch. GitLab fixes
the downstream checkout when creating that pipeline, and its receipt records
+13 −11
Changes for ci/RELEASE.md: 13 added lines, 11 removed lines.
Original line number Diff line number Diff line
# Release an SDK package

1. Review and merge the intended source/version through the release MR.
2. Create its matching protected tag, for example v2.0.0rc5 or v2.0.0.
1. Review the release scope and applicable OEG-driven acceptance results, and
   merge the intended source/version through the release MR.
2. Create its matching protected tag, for example v2.0.0.
   Creating the tag approves publication after successful checks.
   CI builds the wheel and source distribution once, tests those bytes and runs
   the linked SRM consumer. The release matrix and dependency audit also run.
3. Review the green pipeline and applicable OEG-driven acceptance results, then
   play **publish-package**. That one click approves and uploads the tested
   artifacts to the project's GitLab Python package registry.
3. After required CI jobs pass, **publish-package** automatically uploads the
   tested artifacts to the project's GitLab Python package registry.

No qualification file, pipeline-ID input, extra approval form or exception flag
is required. The authorized manual job is the publication decision. Merging the
release MR integrates source; branch pipelines continue to avoid package uploads.
No manual job, qualification file, pipeline-ID input, extra approval form or
exception flag is required. Merging the release MR integrates source; branch
pipelines continue to avoid package uploads.

The publisher automatically saves release-qualification.json with artifact
identity, the GitLab approval job URL, actual SDK JUnit summary and linked
identity, the GitLab publication job URL, actual SDK JUnit summary and linked
downstream pipeline/test results. It uses the current public project APIs without
a new token. Missing or inaccessible reports are recorded as unavailable and
cannot add a new publication gate. External E2E runs remain visible in their
@@ -22,8 +23,9 @@ application, NEF or device acceptance from SDK/consumer results.

The existing required jobs and protected tag/version/source/checksum checks
remain enforced. Report collection changes no test outcome. If evidence is
incomplete, the release owner decides whether to wait or proceed and can explain
the decision in the existing MR. No separate exception workflow is needed.
incomplete, the release owner decides whether to wait or proceed before creating
the tag and can explain the decision in the existing MR. No separate exception
workflow is needed.
If a required job fails, fix/retry it or use an explicitly approved targeted CI
change through the normal review process.

+1 −1
Changes for ci/release.py: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -57,7 +57,7 @@ def verify_release():
            "source_sha": manifest["source_sha"],
            "wheel_sha256": sha256(wheel),
            "release_type": "candidate" if Version(version).is_prerelease else "final",
            "approval_job_url": os.getenv("CI_JOB_URL"),
            "publication_job_url": os.getenv("CI_JOB_URL"),
        },
    )
    receipt = json.loads((OUT / "release-check.json").read_text())
+3 −3
Changes for tests/ci/test_release_reports.py: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
"""One manual publisher records CI evidence without requiring hand-written files."""
"""The publisher records CI evidence without requiring hand-written files."""

import io
import json
@@ -52,7 +52,7 @@ def candidate(tmp_path, monkeypatch):
    return tmp_path, wheel


def test_manual_publication_needs_no_qualification_file_or_extra_parameters(candidate, monkeypatch):
def test_publication_needs_no_qualification_file_or_extra_parameters(candidate, monkeypatch):
    monkeypatch.setattr(release, "collect_reports", lambda: {"sdk_tests": {"total": 374}})
    commands = []
    monkeypatch.setattr(
@@ -65,7 +65,7 @@ def test_manual_publication_needs_no_qualification_file_or_extra_parameters(cand
    release.main()
    output, wheel = candidate
    receipt = json.loads((output / "release-qualification.json").read_text())
    assert receipt["approval_job_url"].endswith("/jobs/20")
    assert receipt["publication_job_url"].endswith("/jobs/20")
    assert receipt["wheel_sha256"] == release.sha256(wheel)
    assert receipt["sdk_tests"]["total"] == 374
    assert commands[-1][1:4] == ["-m", "twine", "upload"]