Add scripts/release-pypi.sh for downloading CI artifacts and uploading to PyPI
Automates the post-tag release flow: resolve the latest successful Build workflow run for a given tag (or take an explicit run ID), download the cibw-* artifacts via gh, flatten wheels/sdists into a dist_<ver>/ directory, and upload via twine from a dedicated .venv-release/ virtualenv. Also documents the release flow in README.md.
This commit is contained in:
40
README.md
40
README.md
@@ -100,6 +100,46 @@ Building a Python wheel distributiion
|
|||||||
pip wheel .
|
pip wheel .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Releasing to PyPI
|
||||||
|
|
||||||
|
Wheels and the sdist are built by the `Build` GitHub Actions workflow
|
||||||
|
(`.github/workflows/wheels.yml`) for every push, including tag pushes. The
|
||||||
|
`scripts/release-pypi.sh` helper downloads those artifacts for a given tag (or
|
||||||
|
explicit run ID) and uploads them to PyPI via `twine`.
|
||||||
|
|
||||||
|
Typical release flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v2.2.1
|
||||||
|
git push origin v2.2.1
|
||||||
|
# wait for the "Build" workflow run to finish successfully on GitHub
|
||||||
|
|
||||||
|
# Download artifacts and upload to PyPI (creates dist_221/ by default).
|
||||||
|
scripts/release-pypi.sh v2.2.1
|
||||||
|
|
||||||
|
# Or, target a specific Actions run id:
|
||||||
|
scripts/release-pypi.sh 1234567890
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements on the release machine:
|
||||||
|
|
||||||
|
- `gh` CLI, authenticated (`gh auth login`)
|
||||||
|
- `python3` (the script creates `.venv-release/` and installs `twine` into it)
|
||||||
|
- PyPI credentials available to `twine`, e.g. `TWINE_USERNAME=__token__` and
|
||||||
|
`TWINE_PASSWORD=<api-token>`, or a configured `~/.pypirc`
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
1. Resolves the latest successful `wheels.yml` run for the tag (or uses the
|
||||||
|
given run ID).
|
||||||
|
2. Downloads `cibw-*` artifacts and flattens all `*.whl` / `*.tar.gz` files
|
||||||
|
into the output directory (default `dist_<digits>` for tags,
|
||||||
|
`dist_run_<id>` for run IDs; pass a second arg to override, and `--force`
|
||||||
|
to reuse a non-empty directory).
|
||||||
|
3. Runs `twine check`, prints the file list, and prompts before running
|
||||||
|
`twine upload`.
|
||||||
|
|
||||||
## Documentation/Example
|
## Documentation/Example
|
||||||
|
|
||||||
There is some basic documentation [here](http://capnproto.github.io/pycapnp/).
|
There is some basic documentation [here](http://capnproto.github.io/pycapnp/).
|
||||||
|
|||||||
153
scripts/release-pypi.sh
Executable file
153
scripts/release-pypi.sh
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Download GitHub Actions build artifacts for a tagged commit (or specific run
|
||||||
|
# ID) and upload the wheels + sdist to PyPI via twine.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/release-pypi.sh <tag-or-run-id> [output-dir] [--force]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# scripts/release-pypi.sh v2.2.1
|
||||||
|
# scripts/release-pypi.sh 2.2.1 dist_221
|
||||||
|
# scripts/release-pypi.sh 1234567890 dist_run_1234567890
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - gh CLI (authenticated; `gh auth status` must succeed)
|
||||||
|
# - python3
|
||||||
|
# - Twine credentials configured in the environment or ~/.pypirc
|
||||||
|
# (TWINE_USERNAME / TWINE_PASSWORD, or an API token).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WORKFLOW_FILE="wheels.yml"
|
||||||
|
ARTIFACT_PATTERN="cibw-*"
|
||||||
|
VENV_DIR=".venv-release"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<EOF
|
||||||
|
Usage: $0 <tag-or-run-id> [output-dir] [--force]
|
||||||
|
|
||||||
|
<tag-or-run-id> Git tag (e.g. v2.2.1 or 2.2.1) or a GitHub Actions run ID.
|
||||||
|
[output-dir] Directory to place wheels/sdist into. Defaults to
|
||||||
|
dist_<digits> for a tag, or dist_run_<id> for a run ID.
|
||||||
|
--force Allow reusing a non-empty output directory.
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "required command '$1' not found in PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE=0
|
||||||
|
POSITIONAL=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--force) FORCE=1 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) POSITIONAL+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ ${#POSITIONAL[@]} -ge 1 && ${#POSITIONAL[@]} -le 2 ]] || usage
|
||||||
|
|
||||||
|
INPUT="${POSITIONAL[0]}"
|
||||||
|
OUTPUT_DIR="${POSITIONAL[1]:-}"
|
||||||
|
|
||||||
|
require_cmd gh
|
||||||
|
require_cmd python3
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
echo ">> verifying gh authentication"
|
||||||
|
gh auth status >/dev/null || die "gh is not authenticated; run 'gh auth login'"
|
||||||
|
|
||||||
|
# Resolve run ID. A bare integer is treated as a run ID; otherwise we look up
|
||||||
|
# the latest successful run on the branch/tag matching the build workflow.
|
||||||
|
if [[ "$INPUT" =~ ^[0-9]+$ ]]; then
|
||||||
|
RUN_ID="$INPUT"
|
||||||
|
DEFAULT_OUT="dist_run_${RUN_ID}"
|
||||||
|
echo ">> using GitHub Actions run id: $RUN_ID"
|
||||||
|
elif [[ "$INPUT" =~ ^v?[0-9].* ]]; then
|
||||||
|
TAG="$INPUT"
|
||||||
|
TAG_NO_V="${TAG#v}"
|
||||||
|
DEFAULT_OUT="dist_${TAG_NO_V//./}"
|
||||||
|
echo ">> resolving latest successful '$WORKFLOW_FILE' run for tag '$TAG'"
|
||||||
|
RUN_ID="$(gh run list \
|
||||||
|
--workflow="$WORKFLOW_FILE" \
|
||||||
|
--branch "$TAG" \
|
||||||
|
--status success \
|
||||||
|
--limit 1 \
|
||||||
|
--json databaseId \
|
||||||
|
--jq '.[0].databaseId')"
|
||||||
|
if [[ -z "${RUN_ID:-}" || "$RUN_ID" == "null" ]]; then
|
||||||
|
die "no successful '$WORKFLOW_FILE' run found for tag '$TAG'"
|
||||||
|
fi
|
||||||
|
echo ">> resolved run id: $RUN_ID"
|
||||||
|
else
|
||||||
|
die "first arg must be a git tag (vX.Y.Z) or a numeric run ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUTPUT_DIR="${OUTPUT_DIR:-$DEFAULT_OUT}"
|
||||||
|
|
||||||
|
if [[ -e "$OUTPUT_DIR" ]]; then
|
||||||
|
if [[ -d "$OUTPUT_DIR" ]]; then
|
||||||
|
if [[ -n "$(ls -A "$OUTPUT_DIR" 2>/dev/null)" && "$FORCE" -ne 1 ]]; then
|
||||||
|
die "'$OUTPUT_DIR' is not empty; pass --force to reuse it"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
die "'$OUTPUT_DIR' exists and is not a directory"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
OUTPUT_DIR_ABS="$(cd "$OUTPUT_DIR" && pwd)"
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d -t pycapnp-release-XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
echo ">> downloading artifacts (pattern '$ARTIFACT_PATTERN') into $TMP_DIR"
|
||||||
|
gh run download "$RUN_ID" --dir "$TMP_DIR" --pattern "$ARTIFACT_PATTERN"
|
||||||
|
|
||||||
|
echo ">> flattening wheels and sdists into $OUTPUT_DIR_ABS"
|
||||||
|
shopt -s nullglob globstar
|
||||||
|
moved=0
|
||||||
|
for f in "$TMP_DIR"/**/*.whl "$TMP_DIR"/**/*.tar.gz; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
mv -n "$f" "$OUTPUT_DIR_ABS/"
|
||||||
|
moved=$((moved + 1))
|
||||||
|
done
|
||||||
|
shopt -u globstar
|
||||||
|
[[ "$moved" -gt 0 ]] || die "no .whl or .tar.gz files found in downloaded artifacts"
|
||||||
|
echo ">> collected $moved files"
|
||||||
|
|
||||||
|
echo ">> setting up release virtualenv at $VENV_DIR"
|
||||||
|
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
|
||||||
|
python3 -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
python -m pip install --quiet --upgrade pip
|
||||||
|
python -m pip install --quiet --upgrade twine
|
||||||
|
|
||||||
|
echo ">> running 'twine check'"
|
||||||
|
python -m twine check "$OUTPUT_DIR_ABS"/*
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Files to upload from $OUTPUT_DIR_ABS:"
|
||||||
|
ls -1 "$OUTPUT_DIR_ABS"
|
||||||
|
echo
|
||||||
|
read -r -p "Upload these to PyPI? [y/N] " reply
|
||||||
|
case "$reply" in
|
||||||
|
[yY]|[yY][eE][sS]) ;;
|
||||||
|
*) echo "aborted; files remain in $OUTPUT_DIR_ABS"; exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ">> uploading to PyPI via twine"
|
||||||
|
python -m twine upload "$OUTPUT_DIR_ABS"/*
|
||||||
|
|
||||||
|
echo ">> done"
|
||||||
Reference in New Issue
Block a user