Skip to content

chore: release v0.9.1 (#172) #19

chore: release v0.9.1 (#172)

chore: release v0.9.1 (#172) #19

Workflow file for this run

name: Publish to PyPI
on:
push:
tags:
- 'v[0-9]*'
# Retry an existing tag without moving it. A publish can fail for reasons that
# have nothing to do with the code — a stale action pin, a PyPI outage — and
# with only the tag trigger the choices were to delete and re-push the tag or
# to burn a version number on a CI fix. release.yml already has this; publish
# is the one that actually needs it, since it is the irreversible half.
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to build and publish (e.g. v0.9.0)'
required: true
type: string
concurrency:
group: pypi-publish-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
env:
# The tag being released, however this run was triggered. Same idiom as
# release.yml so the two workflows read alike.
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
# Before checkout, because checkout resolves the input as an arbitrary ref:
# a branch or SHA would be fetched first and rejected only later by the
# version match. Same strict form as release.yml, so the input contract is
# identical in both.
- name: Validate release tag format
if: github.event_name == 'workflow_dispatch'
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
set -euo pipefail
if [[ ! "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "tag must look like vX.Y.Z, got: $INPUT_TAG" >&2
exit 1
fi
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ env.RELEASE_TAG }}
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'
- name: Install build tooling
run: python -m pip install --upgrade build twine
- name: Verify tag matches pyproject version
run: |
if [[ ! "$RELEASE_TAG" =~ ^v[0-9] ]]; then
echo "Release tag '$RELEASE_TAG' must start with 'v' followed by a digit (e.g. v1.0.0)" >&2
exit 1
fi
tag="${RELEASE_TAG#v}"
pkg_version=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
if [ "$tag" != "$pkg_version" ]; then
echo "Release tag ($tag) does not match pyproject.toml version ($pkg_version)" >&2
exit 1
fi
- name: Build sdist and wheel
run: python -m build
- name: Check distribution metadata
run: python -m twine check --strict dist/*
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/
publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/hotdata
permissions:
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/
# v1.13.0's bundled twine rejects `Metadata-Version: 2.5`, which current
# hatchling emits: `InvalidDistribution: '2.5' is not a valid metadata
# version`. This exact failure blocked hotdata-framework v0.11.0 today
# (hotdata-dev/sdk-python-framework#63). Note the build job's own
# `twine check --strict` passes, because it pip-installs a current twine —
# so the failure appears only at upload, after the tag is public.
- name: Publish via Trusted Publishing
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
# Announce the release in #deploy. Runs only when the publish above
# succeeded. Posts via the shared Hotdata CI Slack app (bot token), so the
# same SLACK_CI_BOT_TOKEN org secret drives notifications across SDK repos.
- name: "Notify #deploy on Slack"
# The publish above is irreversible, so a Slack/token hiccup must not
# mark a successful release as failed and trigger false-alarm retries.
continue-on-error: true
env:
SLACK_CI_BOT_TOKEN: ${{ secrets.SLACK_CI_BOT_TOKEN }}
run: |
set -euo pipefail
if [ -z "${SLACK_CI_BOT_TOKEN:-}" ]; then
echo "SLACK_CI_BOT_TOKEN not set; skipping Slack notification" >&2
exit 0
fi
version="${RELEASE_TAG#v}"
text=":package: *hotdata* \`v${version}\` published to PyPI — <https://pypi.org/project/hotdata/${version}/|PyPI> · <https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}|release notes>"
# #deploy channel ID (rename-proof). chat.postMessage returns HTTP 200
# even on logical errors, so capture the body and assert on .ok —
# echoing Slack's error makes a swallowed (continue-on-error) miss
# debuggable in the job log instead of disappearing silently.
response="$(curl -sS -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer ${SLACK_CI_BOT_TOKEN}" \
-H 'Content-Type: application/json; charset=utf-8' \
--data "$(jq -n --arg ch 'C0ARK84E1D4' --arg text "$text" '{channel:$ch, text:$text}')")"
if ! jq -e '.ok' >/dev/null <<<"$response"; then
echo "Slack notification failed: $(jq -r '.error // "unknown"' <<<"$response")" >&2
exit 1
fi