Skip to content
Closed
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
99 changes: 82 additions & 17 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,64 @@
name: Publish to PyPI

on:
push:
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.16.1)"
required: true
type: string
dry-run:
description: "Build and validate only — skip PyPI upload and tagging"
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
preflight:
name: Preflight checks
runs-on: ubuntu-latest
outputs:
version: ${{ steps.normalize.outputs.version }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Normalize version
id: normalize
run: |
version="${{ inputs.version }}"
version="${version#v}"
echo "version=${version}" >> "$GITHUB_OUTPUT"

- name: Verify version matches codebase
run: |
version="${{ steps.normalize.outputs.version }}"
pyproject_ver=$(python3 -c "
import re, pathlib
m = re.search(r'^version\s*=\s*\"(.+?)\"', pathlib.Path('pyproject.toml').read_text(), re.M)
print(m.group(1))
")
if [ "$pyproject_ver" != "$version" ]; then
echo "::error::Input version ${version} != pyproject.toml version ${pyproject_ver}"
exit 1
fi

- name: Check tag does not exist
run: |
version="${{ steps.normalize.outputs.version }}"
if git ls-remote --tags origin | awk -v tag="refs/tags/${version}" '$2 == tag { found=1 } END { exit !found }'; then
echo "::error::Tag ${version} already exists on remote"
exit 1
fi

build-dashboard:
needs: preflight
name: Build dashboard assets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: ./.github/actions/dashboard-build

Expand All @@ -36,7 +82,7 @@ jobs:
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: ./.github/actions/download-dashboard

Expand Down Expand Up @@ -69,7 +115,7 @@ jobs:
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: ./.github/actions/download-dashboard

Expand Down Expand Up @@ -100,7 +146,7 @@ jobs:
- x86_64-apple-darwin
- aarch64-apple-darwin
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: ./.github/actions/download-dashboard

Expand Down Expand Up @@ -147,7 +193,7 @@ jobs:
include:
- target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: ./.github/actions/download-dashboard

Expand Down Expand Up @@ -189,7 +235,7 @@ jobs:
needs: build-dashboard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: ./.github/actions/download-dashboard

Expand All @@ -208,12 +254,17 @@ jobs:
if-no-files-found: error

publish:
needs: [build-wheels-linux, build-wheels-musllinux, build-wheels-macos, build-wheels-windows, build-sdist]
needs: [preflight, build-wheels-linux, build-wheels-musllinux, build-wheels-macos, build-wheels-windows, build-sdist]
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write
id-token: write
env:
VERSION: ${{ needs.preflight.outputs.version }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Download wheel and sdist artifacts
uses: actions/download-artifact@v8
with:
Expand All @@ -227,13 +278,12 @@ jobs:
for whl in dist/*.whl; do
unzip -qt "$whl"
done
check-wheel-contents dist/*.whl
check-wheel-contents --ignore W009,W010 dist/*.whl

- name: Check PyPI for existing version
id: pypi-check
run: |
version="${GITHUB_REF_NAME}"
python3 - "$version" <<'PYCHECK'
python3 - "$VERSION" <<'PYCHECK'
import hashlib, json, os, sys, urllib.request
from pathlib import Path

Expand Down Expand Up @@ -274,25 +324,40 @@ jobs:
PYCHECK

- name: Publish to PyPI
if: steps.pypi-check.outputs.all_uploaded != 'true'
if: steps.pypi-check.outputs.all_uploaded != 'true' && !inputs.dry-run
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: dist/
skip-existing: true

- name: Verify version on PyPI
if: "!inputs.dry-run"
run: |
version="${GITHUB_REF_NAME}"
for _attempt in 1 2 3 4 5; do
status=$(curl -s -o /dev/null -w '%{http_code}' --head \
--connect-timeout 10 --max-time 30 \
-H "User-Agent: taskito-release/1.0" \
"https://pypi.org/pypi/taskito/${version}/json")
"https://pypi.org/pypi/taskito/${VERSION}/json")
if [ "$status" = "200" ]; then
echo "taskito==${version} live on PyPI"
echo "taskito==${VERSION} live on PyPI"
exit 0
fi
sleep 15
done
echo "::error::taskito==${version} not found on PyPI"
echo "::error::taskito==${VERSION} not found on PyPI"
exit 1

- name: Create git tag
if: "!inputs.dry-run"
run: |
git tag "$VERSION"
git push origin "$VERSION"

- name: Create GitHub release
if: "!inputs.dry-run"
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$VERSION" \
--title "v${VERSION}" \
--generate-notes
17 changes: 1 addition & 16 deletions docs/content/docs/more/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,7 @@ All notable changes to taskito are documented here.

## 0.16.1

Patch release: publish workflow hardening.

### Fixed

- **PyPI publish pre-check.** Publish job now queries PyPI before uploading,
compares SHA-256 hashes per artifact, and skips upload when all wheels are
already present. Prevents `400 File already exists` on workflow reruns.
- **PEP 639 compatibility.** Replaced `twine check` with `check-wheel-contents`
for wheel validation. `twine check` broke on PEP 639 `License-File` metadata
emitted by maturin 1.13.3 (`packaging` library incompatibility). Structural
validation via `check-wheel-contents`; PyPI validates metadata on upload.
- **Timeout guards.** Added `timeout=30` on PyPI API calls and
`--connect-timeout 10 --max-time 30` on verification curl.
- **Dashboard artifact download retry.** Extracted dashboard download into a
reusable composite action with `gh` CLI fallback on transient
`download-artifact` failures (hit on Windows in 0.16.0 publish).
Patch release with no user-facing changes.

## 0.16.0

Expand Down