diff --git a/.github/workflows/fast-mlsirm-publish-pypi.yml b/.github/workflows/fast-mlsirm-publish-pypi.yml index 35ba47ced3..312535da98 100644 --- a/.github/workflows/fast-mlsirm-publish-pypi.yml +++ b/.github/workflows/fast-mlsirm-publish-pypi.yml @@ -129,6 +129,15 @@ jobs: f"release tag does not match project version: expected {expected!r}, got {actual!r}" ) PY + - name: Require reviewed release license helpers + run: | + set -euo pipefail + for helper in tools/select_third_party_license.sh tools/verify_wheel_license.py; do + if [ ! -f "$helper" ] || [ -L "$helper" ]; then + echo "::error::Release source lacks the reviewed license helper: $helper" + exit 1 + fi + done # Reproducible-build clock: the committer time of the exact release # commit, never the wall clock. maturin stamps it into the embedded # CycloneDX SBOM (timestamp/serialNumber) and archive metadata; without it @@ -536,6 +545,9 @@ jobs: # MATURIN_, ...) into the manylinux container, so SOURCE_DATE_EPOCH must # be passed explicitly via docker-options for the Linux legs. Native # macOS/Windows builds inherit it from the job env. + - name: Select reviewed license bundle for wheel target + shell: bash + run: bash tools/select_third_party_license.sh "${{ matrix.target }}" - name: Build wheel uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b env: @@ -548,6 +560,8 @@ jobs: docker-options: -e SOURCE_DATE_EPOCH before-script-linux: $CARGO_BUILD_PYTHON trusted-control/scripts/ci/capture_release_build_scope.py args: --release --out dist -i ${{ matrix.interpreter }} + - name: Verify wheel license bytes + run: python tools/verify_wheel_license.py dist - name: Capture first native-runner build graph if: ${{ runner.os != 'Linux' }} env: @@ -671,6 +685,10 @@ jobs: env: RELEASE_COMMIT: ${{ needs.verify-release.outputs.release_commit }} run: python trusted-control/scripts/ci/release_sdist_consumer.py prepare sdist-proof/sdist.tsv sdist-input sdist-consumer + - name: Select reviewed license bundle for sdist consumer target + working-directory: sdist-consumer/source + shell: bash + run: bash tools/select_third_party_license.sh "${{ matrix.target }}" - name: Build this wheel target from the verified sdist uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b with: @@ -682,6 +700,9 @@ jobs: maturin-version: v1.15.0 docker-options: -e SOURCE_DATE_EPOCH args: --release --out dist-consumer -i ${{ matrix.interpreter }} + - name: Verify sdist consumer wheel license bytes + working-directory: sdist-consumer/source + run: python tools/verify_wheel_license.py dist-consumer - name: Capture target sdist consumer wheel shell: bash env: diff --git a/tests/test_fast_mlsirm_self_hosted_workflows.py b/tests/test_fast_mlsirm_self_hosted_workflows.py index 16905cce94..b456032a3e 100644 --- a/tests/test_fast_mlsirm_self_hosted_workflows.py +++ b/tests/test_fast_mlsirm_self_hosted_workflows.py @@ -1,4 +1,5 @@ from pathlib import Path +import subprocess import textwrap from xml.etree import ElementTree as ET @@ -67,6 +68,33 @@ def test_fast_mlsirm_reusable_workflows_keep_isolated_runner_and_gate_contracts( assert "cargo test --locked --manifest-path crates/fast-mlsirm-py/Cargo.toml" in ci +def test_release_wheel_and_sdist_consumer_keep_selected_license_bytes(): + """Keep target selection and byte verification around both actual builds.""" + text = workflow_text("fast-mlsirm-publish-pypi.yml") + wheel_selection = ' - name: Select reviewed license bundle for wheel target\n shell: bash\n run: bash tools/select_third_party_license.sh "${{ matrix.target }}"\n' + wheel_verification = ' - name: Verify wheel license bytes\n run: python tools/verify_wheel_license.py dist\n' + consumer_selection = ' - name: Select reviewed license bundle for sdist consumer target\n working-directory: sdist-consumer/source\n shell: bash\n run: bash tools/select_third_party_license.sh "${{ matrix.target }}"\n' + consumer_verification = ' - name: Verify sdist consumer wheel license bytes\n working-directory: sdist-consumer/source\n run: python tools/verify_wheel_license.py dist-consumer\n' + for step in (wheel_selection, wheel_verification, consumer_selection, consumer_verification): + assert text.count(step) == 1 + assert text.index(wheel_selection) < text.index(" - name: Build wheel\n") < text.index(wheel_verification) < text.index(" - name: Capture first native-runner build graph\n") + assert text.index(" - name: Verify and unpack the same-run sdist\n") < text.index(consumer_selection) < text.index(" - name: Build this wheel target from the verified sdist\n") < text.index(consumer_verification) < text.index(" - name: Capture target sdist consumer wheel\n") + + +def test_older_release_source_without_license_helpers_fails_before_build(tmp_path): + """Historical commits without the new helpers cannot enter the matrix.""" + text = workflow_text("fast-mlsirm-publish-pypi.yml") + step = text.split(" - name: Require reviewed release license helpers\n", 1)[1] + script = textwrap.dedent(step.split(" run: |\n", 1)[1].split("\n # Reproducible-build clock:", 1)[0]) + assert text.index(" - name: Require reviewed release license helpers\n") < text.index(" sdist:\n") + assert subprocess.run(["bash", "-e", "-c", script], cwd=tmp_path, capture_output=True).returncode == 1 + tools = tmp_path / "tools" + tools.mkdir() + for name in ("select_third_party_license.sh", "verify_wheel_license.py"): + (tools / name).write_text("reviewed helper fixture") + assert subprocess.run(["bash", "-e", "-c", script], cwd=tmp_path, capture_output=True).returncode == 0 + + def test_disabled_release_legs_fail_the_caller_instead_of_reporting_success(): """Expose the intentional release hold as a failing required job.""" text = workflow_text("fast-mlsirm-publish-pypi.yml")