Skip to content

Release

Release #9

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
release_tag:
description: Annotated tag on main to publish
required: true
type: string
candidate_run_id:
description: Prior release run whose tested candidate should be recovered
required: false
default: ""
type: string
permissions:
contents: read
jobs:
build-candidate:
name: Build release candidate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
candidate-sha: ${{ steps.candidate.outputs.sha }}
permissions:
actions: read
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
if: inputs.candidate_run_id == ''
with:
ref: ${{ inputs.release_tag }}
fetch-depth: 0
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
if: inputs.candidate_run_id != ''
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Validate recovery run id
if: inputs.candidate_run_id != ''
env:
CANDIDATE_RUN_ID: ${{ inputs.candidate_run_id }}
run: |
[[ "$CANDIDATE_RUN_ID" =~ ^[0-9]+$ ]] || { echo "candidate run id must be numeric" >&2; exit 1; }
- name: Recover the prior tested candidate
if: inputs.candidate_run_id != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-candidate
github-token: ${{ github.token }}
run-id: ${{ inputs.candidate_run_id }}
- name: Install checksum-pinned GitHub CLI
env:
GH_VERSION: "2.96.0"
GH_SHA256: 83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60
run: |
curl --fail --location --silent --show-error "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" --output "$RUNNER_TEMP/gh.tar.gz"
echo "$GH_SHA256 $RUNNER_TEMP/gh.tar.gz" | sha256sum --check --strict
tar -xzf "$RUNNER_TEMP/gh.tar.gz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/gh_${GH_VERSION}_linux_amd64/bin" >> "$GITHUB_PATH"
- name: Validate candidate tag and main ancestry
id: candidate
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
CANDIDATE_RUN_ID: ${{ inputs.candidate_run_id }}
run: |
git fetch origin main --no-tags
git fetch origin "refs/tags/$RELEASE_TAG:refs/tags/candidate-check" --force
CANDIDATE_SHA="$(git rev-parse 'refs/tags/candidate-check^{commit}')"
git merge-base --is-ancestor "$CANDIDATE_SHA" origin/main
git show "$CANDIDATE_SHA:pyproject.toml" > "$RUNNER_TEMP/candidate-pyproject.toml"
python scripts/verify_distribution.py release-tag "$RELEASE_TAG" --pyproject "$RUNNER_TEMP/candidate-pyproject.toml"
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
echo "release already exists for $RELEASE_TAG" >&2
exit 1
fi
if [[ -n "$CANDIDATE_RUN_ID" ]]; then
python scripts/verify_distribution.py verify-candidate dist release/CANDIDATE.json --tag "$RELEASE_TAG" --commit "$CANDIDATE_SHA"
python scripts/verify_distribution.py verify-checksums dist release/SHA256SUMS
python scripts/verify_distribution.py archives dist
VERSION="${RELEASE_TAG#v}"
curl --fail --location --retry 4 --retry-delay 5 "https://test.pypi.org/pypi/pebra/$VERSION/json" --output "$RUNNER_TEMP/testpypi.json"
python scripts/verify_distribution.py index-digests dist "$RUNNER_TEMP/testpypi.json"
fi
echo "sha=$CANDIDATE_SHA" >> "$GITHUB_OUTPUT"
- name: Build and validate candidate distributions
if: inputs.candidate_run_id == ''
env:
RELEASE_TAG: ${{ inputs.release_tag }}
CANDIDATE_SHA: ${{ steps.candidate.outputs.sha }}
run: |
python -m pip install --require-hashes -r requirements-release.txt
python -m build --no-isolation
python -m twine check dist/*
python scripts/verify_distribution.py archives dist
python scripts/verify_distribution.py checksums dist
mkdir release
mv dist/SHA256SUMS release/SHA256SUMS
python scripts/verify_distribution.py candidate-manifest dist release/CANDIDATE.json --tag "$RELEASE_TAG" --commit "$CANDIDATE_SHA"
- name: Attest public candidate artifacts
if: github.event.repository.visibility == 'public' && inputs.candidate_run_id == ''
uses: actions/attest@f6bf1532d7d6793fce74eac584813a8eee607999 # v4
with:
subject-path: dist/*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-candidate
path: |
dist/*
release/SHA256SUMS
release/CANDIDATE.json
if-no-files-found: error
retention-days: 14
publish-testpypi:
name: Publish candidate to TestPyPI
needs: build-candidate
if: inputs.candidate_run_id == ''
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-candidate
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
repository-url: https://test.pypi.org/legacy/
packages-dir: dist
publish-pypi:
name: Verify and publish tested bytes to PyPI
needs: [build-candidate, publish-testpypi]
if: ${{ always() && needs.build-candidate.result == 'success' && (needs.publish-testpypi.result == 'success' || needs.publish-testpypi.result == 'skipped') }}
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.build-candidate.outputs.candidate-sha }}
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-candidate
- name: Verify candidate identity, checksums, and distributions
env:
RELEASE_TAG: ${{ inputs.release_tag }}
CANDIDATE_SHA: ${{ needs.build-candidate.outputs.candidate-sha }}
run: |
git fetch origin "refs/tags/$RELEASE_TAG:refs/tags/release-check" --force
test "$(git rev-parse refs/tags/release-check^{commit})" = "$CANDIDATE_SHA" || { echo "remote tag moved after candidate build" >&2; exit 1; }
python -m pip install --require-hashes -r requirements-release.txt
python scripts/verify_distribution.py verify-candidate dist release/CANDIDATE.json --tag "$RELEASE_TAG" --commit "$CANDIDATE_SHA"
python scripts/verify_distribution.py verify-checksums dist release/SHA256SUMS
python scripts/verify_distribution.py archives dist
python -m twine check dist/*
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: dist
create-github-release:
name: Create GitHub release
needs: [build-candidate, publish-pypi]
if: ${{ always() && needs.build-candidate.result == 'success' && needs.publish-pypi.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.build-candidate.outputs.candidate-sha }}
fetch-depth: 0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-candidate
- name: Install checksum-pinned GitHub CLI
env:
GH_VERSION: "2.96.0"
GH_SHA256: 83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60
run: |
curl --fail --location --silent --show-error "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" --output "$RUNNER_TEMP/gh.tar.gz"
echo "$GH_SHA256 $RUNNER_TEMP/gh.tar.gz" | sha256sum --check --strict
tar -xzf "$RUNNER_TEMP/gh.tar.gz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/gh_${GH_VERSION}_linux_amd64/bin" >> "$GITHUB_PATH"
- name: Create release from the published candidate
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
CANDIDATE_SHA: ${{ needs.build-candidate.outputs.candidate-sha }}
run: |
git fetch origin "refs/tags/$RELEASE_TAG:refs/tags/release-check" --force
test "$(git rev-parse refs/tags/release-check^{commit})" = "$CANDIDATE_SHA" || { echo "remote tag moved after candidate build" >&2; exit 1; }
gh release create "$RELEASE_TAG" dist/*.whl dist/*.tar.gz release/SHA256SUMS release/CANDIDATE.json --verify-tag --generate-notes --title "$RELEASE_TAG"