Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 70 additions & 17 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ on:
- testpypi
- pypi
default: testpypi
release_tag:
description: "Annotated version tag to publish when dispatching from the default branch"
required: false
type: string
default: ""

permissions:
contents: read
Expand All @@ -61,11 +66,15 @@ jobs:
timeout-minutes: 10
outputs:
version: ${{ steps.metadata.outputs.version }}
source_commit: ${{ steps.metadata.outputs.source_commit }}
release_tag: ${{ steps.metadata.outputs.release_tag }}
source_ref_type: ${{ steps.metadata.outputs.source_ref_type }}
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ inputs.release_tag || github.ref }}

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -74,28 +83,54 @@ jobs:

- name: Read package version
id: metadata
run: echo "version=$(tr -d '\\r\\n' < VERSION)" >> "$GITHUB_OUTPUT"
env:
REQUESTED_RELEASE_TAG: ${{ inputs.release_tag || '' }}
run: |
version="$(tr -d '\\r\\n' < VERSION)"
release_tag="$REQUESTED_RELEASE_TAG"
if [[ -n "$release_tag" ]]; then
source_ref_type=tag
source_commit="$(git rev-parse --verify "refs/tags/$release_tag^{}")"
else
source_ref_type="$GITHUB_REF_TYPE"
source_commit="$GITHUB_SHA"
if [[ "$source_ref_type" == tag ]]; then
release_tag="$GITHUB_REF_NAME"
fi
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT"
echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT"
echo "source_ref_type=$source_ref_type" >> "$GITHUB_OUTPUT"

- name: Validate release ref
env:
PUBLISH_TARGET: ${{ inputs.publish_target || '' }}
REQUESTED_RELEASE_TAG: ${{ inputs.release_tag || '' }}
SOURCE_REF_TYPE: ${{ steps.metadata.outputs.source_ref_type }}
RELEASE_TAG: ${{ steps.metadata.outputs.release_tag }}
VERSION: ${{ steps.metadata.outputs.version }}
run: |
if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" != "v${{ steps.metadata.outputs.version }}" ]]; then
echo "Release tag must be v${{ steps.metadata.outputs.version }}; got $GITHUB_REF_NAME" >&2
if [[ "$SOURCE_REF_TYPE" == "tag" && "$RELEASE_TAG" != "v$VERSION" ]]; then
echo "Release tag must be v$VERSION; got $RELEASE_TAG" >&2
exit 1
fi
if [[ "$PUBLISH_TARGET" == "pypi" && "$GITHUB_REF_TYPE" != "tag" ]]; then
echo "PyPI publication requires dispatching this workflow from the matching version tag." >&2
if [[ "$PUBLISH_TARGET" == "pypi" && "$SOURCE_REF_TYPE" != "tag" ]]; then
echo "PyPI publication requires a matching version tag." >&2
exit 1
fi
if [[ "$PUBLISH_TARGET" == "pypi" && "$GITHUB_EVENT_NAME" == workflow_dispatch && -z "$REQUESTED_RELEASE_TAG" ]]; then
echo "Manual PyPI publication requires the release_tag input." >&2
exit 1
fi

- name: Validate changelog
run: python scripts/validate_changelog.py

- name: Validate tagged release notes
if: ${{ github.ref_type == 'tag' }}
if: ${{ steps.metadata.outputs.source_ref_type == 'tag' }}
env:
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_TAG: ${{ steps.metadata.outputs.release_tag }}
run: python scripts/validate_release_ref.py

- name: Prepare clean artifact destination
Expand All @@ -111,12 +146,13 @@ jobs:
# Keep the reproducibility input numeric; push-event timestamps are
# ISO-8601 strings and are not accepted by wheel/build backends.
SOURCE_DATE_EPOCH: '0'
SOURCE_COMMIT: ${{ steps.metadata.outputs.source_commit }}
run: |
rm -rf dist-a dist-b "$RUNNER_TEMP/base-cli-build-a" "$RUNNER_TEMP/base-cli-build-b"
mkdir -p dist-a dist-b
mkdir -p "$RUNNER_TEMP/base-cli-build-a" "$RUNNER_TEMP/base-cli-build-b"
git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/base-cli-build-a"
git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/base-cli-build-b"
git archive "$SOURCE_COMMIT" | tar -x -C "$RUNNER_TEMP/base-cli-build-a"
git archive "$SOURCE_COMMIT" | tar -x -C "$RUNNER_TEMP/base-cli-build-b"
(cd "$RUNNER_TEMP/base-cli-build-a" && python -m build --sdist --wheel --outdir "$GITHUB_WORKSPACE/dist-a")
(cd "$RUNNER_TEMP/base-cli-build-b" && python -m build --sdist --wheel --outdir "$GITHUB_WORKSPACE/dist-b")
python - <<'PY'
Expand Down Expand Up @@ -155,13 +191,13 @@ jobs:

- name: Generate release checksums and SPDX SBOM
env:
SOURCE_REVISION: ${{ github.sha }}
SOURCE_REVISION: ${{ steps.metadata.outputs.source_commit }}
SOURCE_DATE_EPOCH: '0'
run: python scripts/generate_release_metadata.py dist

- name: Validate release checksums and SPDX SBOM
env:
SOURCE_REVISION: ${{ github.sha }}
SOURCE_REVISION: ${{ steps.metadata.outputs.source_commit }}
run: python scripts/validate_release_metadata.py dist

- name: Upload reviewed distributions
Expand Down Expand Up @@ -199,6 +235,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ needs.build.outputs.source_commit }}

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand Down Expand Up @@ -250,14 +287,23 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ needs.build.outputs.source_commit }}

- name: Fetch trusted main history
run: git fetch --no-tags --prune origin "refs/heads/main:refs/remotes/origin/main"

- name: Verify release provenance
env:
PUBLISH_TARGET: ${{ inputs.publish_target || '' }}
run: python scripts/validate_release_provenance.py
SOURCE_REF_TYPE: ${{ needs.build.outputs.source_ref_type }}
RELEASE_TAG: ${{ needs.build.outputs.release_tag }}
SOURCE_COMMIT: ${{ needs.build.outputs.source_commit }}
run: |
python scripts/validate_release_provenance.py \
--ref-type "$SOURCE_REF_TYPE" \
--tag "$RELEASE_TAG" \
--publish-target "$PUBLISH_TARGET" \
--source-commit "$SOURCE_COMMIT"

publish:
name: Publish reviewed distribution
Expand Down Expand Up @@ -328,7 +374,7 @@ jobs:
release:
name: Create GitHub Release
needs: [build, smoke, provenance, publish, attest]
if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
if: ${{ (github.event_name == 'push' && github.ref_type == 'tag') || (github.event_name == 'workflow_dispatch' && inputs.publish_target == 'pypi' && inputs.release_tag != '') }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
Expand All @@ -337,6 +383,9 @@ jobs:
steps:
- name: Check out the tag source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ needs.build.outputs.source_commit }}

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -358,13 +407,17 @@ jobs:
- name: Verify and create immutable GitHub Release
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REVISION: ${{ needs.build.outputs.source_commit }}
RELEASE_TAG: ${{ needs.build.outputs.release_tag }}
SOURCE_COMMIT: ${{ needs.build.outputs.source_commit }}
run: |
set -euo pipefail
tag="$GITHUB_REF_NAME"
tag="$RELEASE_TAG"
source_commit="$SOURCE_COMMIT"
assets=(dist/*.whl dist/*.tar.gz dist/SHA256SUMS dist/SBOM.spdx.json dist/RELEASE-BOM-ROW.json)
tag_commit="$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha)"
if [[ "$tag_commit" != "$GITHUB_SHA" ]]; then
echo "Release tag $tag resolves to $tag_commit, not reviewed commit $GITHUB_SHA." >&2
if [[ "$tag_commit" != "$source_commit" ]]; then
echo "Release tag $tag resolves to $tag_commit, not reviewed commit $source_commit." >&2
exit 1
fi

Expand Down Expand Up @@ -424,7 +477,7 @@ jobs:
--release-json "$release_json" \
--version-file VERSION \
--tag "$tag" \
--source-commit "$GITHUB_SHA" \
--source-commit "$source_commit" \
--resolved-tag-commit "$tag_commit"
}

Expand Down
13 changes: 13 additions & 0 deletions tests/test_package_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ def test_package_workflow_uses_numeric_reproducibility_epoch() -> None:
assert workflow.count("SOURCE_DATE_EPOCH: '0'") == 2


def test_package_workflow_can_recover_an_immutable_tag_from_default_branch() -> None:
workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/package.yml").read_text(encoding="utf-8")

assert "release_tag:" in workflow
assert "ref: ${{ inputs.release_tag || github.ref }}" in workflow
assert "source_commit: ${{ steps.metadata.outputs.source_commit }}" in workflow
assert "SOURCE_COMMIT: ${{ steps.metadata.outputs.source_commit }}" in workflow
assert 'git archive "$SOURCE_COMMIT"' in workflow
assert "Manual PyPI publication requires the release_tag input." in workflow
assert '--source-commit "$SOURCE_COMMIT"' in workflow


def test_package_workflow_does_not_replace_published_release_assets() -> None:
workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/package.yml").read_text(encoding="utf-8")
verifier = (Path(__file__).resolve().parents[1] / "scripts/verify_release_assets.py").read_text(encoding="utf-8")
Expand All @@ -23,6 +35,7 @@ def test_package_workflow_does_not_replace_published_release_assets() -> None:
assert "gh attestation verify" in workflow
assert '--source-digest "$GITHUB_SHA"' in workflow
assert '--source-ref "$GITHUB_REF"' in workflow
assert '--source-commit "$source_commit"' in workflow
assert "--clobber" not in workflow


Expand Down
Loading