Skip to content

Publish to PyPI

Publish to PyPI #2

Workflow file for this run

name: Publish to PyPI
# Manually triggered release, mirroring awslabs/deequ's publish flow: the
# operator supplies the release version, the workflow sets it, builds, and
# publishes to PyPI, then tags the release and opens a follow-up PR that bumps
# the version in the repo. The PyPI token is never stored in GitHub — it is
# fetched at run time from AWS Secrets Manager via OIDC and passed to Poetry
# through an env var.
on:
workflow_dispatch:
inputs:
release_version:
description: "Release version, e.g. 1.6.0 (must be a new version, not already on PyPI)"
required: true
dry_run:
description: "Dry run (build only, skip the PyPI upload, tag, and version-bump PR)"
type: boolean
default: true
permissions:
id-token: write # assume the AWS role via OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15
# Real uploads run in the protected environment (required reviewer); a dry
# run resolves to '' (no environment, no approval, no credentials).
environment: ${{ !inputs.dry_run && 'pypi-release' || '' }}
outputs:
version: ${{ steps.version.outputs.version }}
sha: ${{ steps.sha.outputs.sha }}
steps:
- name: Require dispatch from master for a real release
# The pypi-release environment only permits deployments from master, and
# GitHub matches that against the dispatch ref (not the checkout). Fail
# early with a clear message instead of an opaque environment rejection.
if: ${{ !inputs.dry_run && github.ref != 'refs/heads/master' }}
run: |
echo "::error::Real releases must be dispatched from the master branch (got ${GITHUB_REF})." >&2
exit 1
- name: Checkout master
# Always release from master regardless of the branch the dispatch UI
# selected, so the build/tag/bump reflect the mainline.
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: master
persist-credentials: false
- name: Record release SHA
# Pin the exact commit built/published so the tag job tags THIS commit,
# even if master advances during the approval wait.
id: sha
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.9"
- name: Install Poetry
run: pip install poetry==1.7.1
- name: Normalize and validate version
# Strip an accidental leading 'v', reject anything that isn't a plain
# release version, and expose the canonical value for every downstream
# step (build, tag, bump, URL) so tag/__version__/wheel cannot diverge.
id: version
run: |
V="${RAW_VERSION#v}"
if ! printf '%s' "$V" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([abc]|rc|\.post|\.dev)?[0-9]*$'; then
echo "::error::Invalid release version '${RAW_VERSION}' (expected e.g. 1.6.0)" >&2
exit 1
fi
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "Release version: $V"
env:
RAW_VERSION: ${{ inputs.release_version }}
- name: Set release version
# Set the canonical version in both places pydeequ keeps it, then assert
# they agree with the wheel version poetry will build.
run: |
poetry version "${VERSION}"
sed -i "s/^__version__ = .*/__version__ = \"${VERSION}\"/" pydeequ/__init__.py
BUILT=$(poetry version --short)
if [ "$BUILT" != "${VERSION}" ]; then
echo "::error::pyproject version $BUILT != requested ${VERSION}" >&2
exit 1
fi
grep -q "__version__ = \"${VERSION}\"" pydeequ/__init__.py || {
echo "::error::__init__ __version__ was not updated to ${VERSION}" >&2; exit 1; }
env:
VERSION: ${{ steps.version.outputs.version }}
- name: Build sdist + wheel
run: poetry build
- name: Configure AWS credentials (OIDC)
if: ${{ !inputs.dry_run }}
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::059379938386:role/GitHubActionsPyDeequPublish
aws-region: us-east-1
- name: Publish to PyPI
if: ${{ !inputs.dry_run }}
# No --skip-existing: a version already on PyPI must fail loudly here so
# the tag/bump never run for an artifact this release did not upload.
# Token → POETRY_PYPI_TOKEN_PYPI env (read natively by poetry), never on
# argv, never in $GITHUB_OUTPUT, never written to a file. Masked first.
run: |
PYPI_TOKEN=$(aws secretsmanager get-secret-value --secret-id pypi_token --query SecretString --output text)
echo "::add-mask::$PYPI_TOKEN"
export POETRY_PYPI_TOKEN_PYPI="$PYPI_TOKEN"
poetry publish --no-interaction
- name: Summary
run: |
if [ "${DRY_RUN}" = "true" ]; then
echo "### Dry run completed for ${VERSION} (not published)" >> "$GITHUB_STEP_SUMMARY"
else
echo "### Published ${VERSION} to PyPI" >> "$GITHUB_STEP_SUMMARY"
echo "View at: https://pypi.org/project/pydeequ/${VERSION}/" >> "$GITHUB_STEP_SUMMARY"
fi
env:
DRY_RUN: ${{ inputs.dry_run }}
VERSION: ${{ steps.version.outputs.version }}
tag-and-bump:
# Runs ONLY after a successful real publish (needs: publish gates on
# success). Creates the release tag as a record of what is now on PyPI (tag
# after publish, never before), then opens a PR recording the version.
needs: publish
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: write
env:
VERSION: ${{ needs.publish.outputs.version }}
RELEASE_SHA: ${{ needs.publish.outputs.sha }}
steps:
- name: Checkout the released commit
# The exact SHA the publish job built/uploaded — not a fresh master tip,
# so a merge during the approval wait can't move the tag off the release.
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.publish.outputs.sha }}
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.9"
- name: Install Poetry
run: pip install poetry==1.7.1
- name: Tag the published release
# Tag the exact published commit, now that PyPI has the artifact.
# Idempotent: skip if the tag already exists remotely.
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code --tags origin "v${VERSION}" >/dev/null 2>&1; then
echo "Tag v${VERSION} already exists — skipping."
else
git tag -a "v${VERSION}" "${RELEASE_SHA}" -m "Release ${VERSION}"
git push origin "v${VERSION}"
fi
- name: Bump version and open PR
# Idempotent: skip if the bump branch/PR already exist or master is
# already at this version (e.g. a re-run after a partial failure).
run: |
BRANCH_NAME="bump-version-${VERSION}"
if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then
echo "Branch ${BRANCH_NAME} already exists — skipping bump PR."
exit 0
fi
git fetch origin master
git checkout -b "${BRANCH_NAME}" origin/master
poetry version "${VERSION}"
sed -i "s/^__version__ = .*/__version__ = \"${VERSION}\"/" pydeequ/__init__.py
if git diff --quiet -- pyproject.toml pydeequ/__init__.py; then
echo "master already at ${VERSION} — nothing to bump."
exit 0
fi
git add pyproject.toml pydeequ/__init__.py
git commit -m "Update version to ${VERSION}"
git push origin "${BRANCH_NAME}"
gh pr create \
--base master \
--head "${BRANCH_NAME}" \
--title "Update version to ${VERSION}" \
--body "Automated version bump after publishing ${VERSION} to PyPI."
env:
GH_TOKEN: ${{ github.token }}