Repository navigation
138 lines (123 loc) · 5.66 KB
/
Copy pathpublish.yml
File metadata and controls
138 lines (123 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # 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