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
31 changes: 31 additions & 0 deletions .github/scripts/compile-release-locks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Regenerates the hash-locked requirements of .github/workflows/build-release.yml:
# requirements-release-test.txt from requirements-ci.txt and PIP below
# requirements-release-build.txt from BUILD_TOOLS and PIP below
# Resolves inside linux/amd64 CPython 3.10, the interpreter and platform of the release job,
# so environment markers and wheel tags match what the runner installs. Requires Docker.
# Existing pins are kept; pass --upgrade to move every package to its latest version.
# Usage, from the repository root: bash .github/scripts/compile-release-locks.sh [--upgrade]
set -euo pipefail

# setuptools and wheel must satisfy [build-system] requires in pyproject.toml: the build job
# runs python -m build --no-isolation, which fails if the installed versions do not.
BUILD_TOOLS='build==1.6.1
setuptools==84.2.1
wheel==0.48.0'
PIP='pip==26.3'

cd "$(dirname "$0")/../.."

docker run --rm --platform linux/amd64 -v "$PWD":/src -w /src \
-e CUSTOM_COMPILE_COMMAND="bash .github/scripts/compile-release-locks.sh" \
-e BUILD_TOOLS="$BUILD_TOOLS" -e PIP="$PIP" -e EXTRA_ARGS="${1:-}" \
python:3.10-slim bash -euo pipefail -c '
pip install --quiet --root-user-action=ignore pip-tools==7.6.2
compile() {
pip-compile --quiet --generate-hashes --allow-unsafe --strip-extras --no-emit-index-url \
--pip-args="--only-binary :all:" $EXTRA_ARGS "$@"
}
printf "%s\n" "$PIP" | compile -o requirements-release-test.txt requirements-ci.txt -
printf "%s\n" "$BUILD_TOOLS" "$PIP" | compile -o requirements-release-build.txt -
'
91 changes: 91 additions & 0 deletions .github/scripts/verify_dist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Checks the release artifacts in dist/ before they are handed to the publish job.
Fails unless the directory holds exactly the wheel and the sdist of the expected version,
both archives contain only the entries the package is meant to ship, and the version in
the metadata and in checkout_sdk/properties.py matches. Standard library only, so it adds
nothing to the release job's locked dependencies.
Run from the repository root: python -I .github/scripts/verify_dist.py <version>
"""
import email.parser
import pathlib
import re
import sys
import tarfile
import zipfile

PACKAGE = 'checkout_sdk'
DIST = pathlib.Path('dist')
VERSION_PATTERN = re.compile(r'\d+\.\d+\.\d+((a|b|rc)\d+)?(\.dev\d+)?', re.ASCII)


def fail(message):
sys.exit(f'verify_dist: {message}')


def read_metadata(raw):
return email.parser.Parser().parsestr(raw.decode('utf-8'))


def check_metadata(metadata, version, source):
if metadata['Name'] != PACKAGE:
fail(f'{source}: Name is {metadata["Name"]!r}, expected {PACKAGE!r}')
if metadata['Version'] != version:
fail(f'{source}: Version is {metadata["Version"]!r}, expected {version!r}')


def check_properties(raw, version, source):
expected = f'VERSION = "{version}"'
if expected not in raw.decode('utf-8'):
fail(f'{source}: checkout_sdk/properties.py does not contain {expected}')


def check_wheel(path, version):
allowed = {PACKAGE, f'{PACKAGE}-{version}.dist-info'}
with zipfile.ZipFile(path) as wheel:
names = wheel.namelist()
unexpected = sorted({name.split('/')[0] for name in names} - allowed)
if unexpected:
fail(f'{path.name}: unexpected top-level entries {unexpected}')
check_metadata(read_metadata(wheel.read(f'{PACKAGE}-{version}.dist-info/METADATA')),
version, path.name)
check_properties(wheel.read(f'{PACKAGE}/properties.py'), version, path.name)
return len(names)


def check_sdist(path, version):
root = f'{PACKAGE}-{version}'
allowed = {'LICENSE.md', 'PKG-INFO', 'README.md', PACKAGE, f'{PACKAGE}.egg-info',
'pyproject.toml', 'setup.cfg', 'setup.py'}
with tarfile.open(path) as sdist:
Comment thread
david-ruiz-cko marked this conversation as resolved.
Fixed
members = [member for member in sdist.getmembers() if member.name != root]
for member in members:
parts = member.name.split('/')
if parts[0] != root or parts[1] not in allowed:
fail(f'{path.name}: unexpected entry {member.name}')
if not (member.isfile() or member.isdir()):
fail(f'{path.name}: {member.name} is not a regular file or directory')
check_metadata(read_metadata(sdist.extractfile(f'{root}/PKG-INFO').read()),
version, path.name)
check_properties(sdist.extractfile(f'{root}/{PACKAGE}/properties.py').read(),
version, path.name)
return len(members)


def main(version):
if not VERSION_PATTERN.fullmatch(version):
fail(f'{version!r} is not a release or dev version')
# The version is only compared with file names; the files opened come from the listing.
files = sorted(DIST.iterdir())
expected = sorted([f'{PACKAGE}-{version}-py3-none-any.whl', f'{PACKAGE}-{version}.tar.gz'])
if [path.name for path in files] != expected:
fail(f'{DIST}: found {[path.name for path in files]}, expected exactly {expected}')
wheel, sdist = files # '-py3' sorts before '.tar', so the wheel comes first
print(f'{wheel.name}: {check_wheel(wheel, version)} entries OK')
print(f'{sdist.name}: {check_sdist(sdist, version)} entries OK')


if __name__ == '__main__':
if len(sys.argv) != 2:
fail('usage: verify_dist.py <version>')
main(sys.argv[1])
170 changes: 139 additions & 31 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,104 @@
- main
paths:
- checkout_sdk/properties.py
# Dry run from any branch: builds VERSION.devN and uploads it to TestPyPI only.
workflow_dispatch:

# Read-only by default; jobs that need more declare it.
permissions:
contents: read # checkout only

concurrency:
group: build-release-${{ github.ref }}
cancel-in-progress: false

# Installs use the hash-locked requirement files. Regenerate them with
# bash .github/scripts/compile-release-locks.sh.
jobs:
deploy:
if: github.ref == 'refs/heads/main'
# Builds and checks the distributions and uploads them as the dist artifact.
build:
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
# The repository's default GITHUB_TOKEN is read-only. create-release needs
# write access to contents to create the version tag and GitHub release.
permissions:
contents: write
outputs:
version: ${{ steps.read-version.outputs.version }}
wheel-sha256: ${{ steps.hash-wheel.outputs.wheel-sha256 }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: setup-python
uses: actions/setup-python@v2
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.10'
# Wheels only, as in build-main.yml: this job also holds the PyPI credentials.
# The tooling is pinned to exact versions (Sonar S8544).
- id: install-tooling
run: |
python -m pip install --only-binary :all: pip==26.2.2
pip install --only-binary :all: wheel==0.48.0
pip install --only-binary :all: twine==7.0.0
pip install --only-binary :all: build==1.6.1
python -m pip install --only-binary :all: --require-hashes -r requirements-release-build.txt
- id: read-version
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }}
run: |
version=$(sed -n 's/^VERSION = "\(.*\)"$/\1/p' checkout_sdk/properties.py)
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]]; then
echo "::error::Unexpected VERSION in checkout_sdk/properties.py: '$version'"
exit 1
fi
if [ "$DRY_RUN" = true ]; then
version="${version}.dev${GITHUB_RUN_NUMBER}"
sed -i "s/^VERSION = \".*\"$/VERSION = \"${version}\"/" checkout_sdk/properties.py
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Building $version"
# --no-isolation builds with the locked setuptools and wheel instead of letting build
# download them, and fails if they do not match [build-system] in pyproject.toml.
- id: build-dist
run: python -m build --no-isolation
- id: verify-dist
env:
VERSION: ${{ steps.read-version.outputs.version }}
run: python -I .github/scripts/verify_dist.py "$VERSION"
- id: hash-wheel
run: |
wheels=(dist/*.whl)
echo "wheel-sha256=$(sha256sum "${wheels[0]}" | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT"
- id: upload-dist
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist/
if-no-files-found: error

# Runs the test suite against the sandbox, using the built wheel.
test:
needs: build
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: setup-python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.10'
- id: install-project-dependencies
run: |
pip install --only-binary :all: -r requirements-ci.txt
python -m pip install --only-binary :all: --require-hashes -r requirements-release-test.txt
# Installs nothing; fails if requirements-ci.txt changed without regenerating the lock.
python -m pip install --only-binary :all: --no-index --dry-run -r requirements-ci.txt
- id: download-dist
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist/
# Moves the source package aside so the tests import the installed wheel.
- id: install-dist
env:
WHEEL_SHA256: ${{ needs.build.outputs.wheel-sha256 }}
run: |
wheels=(dist/*.whl)
echo "$PWD/${wheels[0]} --hash=sha256:$WHEEL_SHA256" > "$RUNNER_TEMP/dist-requirements.txt"
python -m pip install --only-binary :all: --no-deps --no-index --require-hashes -r "$RUNNER_TEMP/dist-requirements.txt"
mv checkout_sdk "$RUNNER_TEMP/checkout_sdk-source"
python -c 'import checkout_sdk, sysconfig; assert checkout_sdk.__file__.startswith(sysconfig.get_paths()["purelib"]), checkout_sdk.__file__'
- id: testing
env:
CHECKOUT_PROCESSING_CHANNEL_ID: ${{ secrets.IT_CHECKOUT_PROCESSING_CHANNEL_ID }}
Expand All @@ -48,21 +120,57 @@
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }}
CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }}
run: python -m pytest -x --import-mode=append tests/
- id: read-version
run: echo "CURRENT_VERSION=$(grep -o '".*"' checkout_sdk/properties.py | sed 's/"//g' | tr -d \\n)" >> $GITHUB_ENV
- id: print-version
run: echo "Releasing $CURRENT_VERSION"
- id: build-dist
run: python -m build
- id: upload-dist
run: twine upload dist/* -u ${{ secrets.PY_PI_USER }} -p ${{ secrets.PY_PI_PW }}

# Publishes the dist artifact with PyPI Trusted Publishing.
publish-testpypi:
if: github.event_name == 'workflow_dispatch'
needs: [build, test]
runs-on: ubuntu-22.04
environment:
name: testpypi
url: https://test.pypi.org/project/checkout-sdk/${{ needs.build.outputs.version }}/
permissions:
id-token: write # Trusted Publishing OIDC token
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
with:
repository-url: https://test.pypi.org/legacy/
print-hash: true

publish-pypi:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [build, test]
runs-on: ubuntu-22.04
environment:
name: pypi
url: https://pypi.org/project/checkout-sdk/${{ needs.build.outputs.version }}/
permissions:
id-token: write # Trusted Publishing OIDC token
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
with:
print-hash: true

release:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [build, publish-pypi]
runs-on: ubuntu-22.04
permissions:
contents: write # gh release create
steps:
- id: create-release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.CURRENT_VERSION }}
release_name: ${{ env.CURRENT_VERSION }}
body: ${{ github.event.head_commit.message }}
draft: false
prerelease: false
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.build.outputs.version }}
NOTES: ${{ github.event.head_commit.message }}
run: |
gh release create "$VERSION" --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" \
--title "$VERSION" --notes "$NOTES"
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build-system]
# Exact pins for reproducible builds.
# Keep in sync with BUILD_TOOLS in .github/scripts/compile-release-locks.sh.
requires = ["setuptools==84.2.1", "wheel==0.48.0"]
build-backend = "setuptools.build_meta"
Loading
Loading