Skip to content

Commit 4bca26f

Browse files
ci: run Docker/AUR/Homebrew after Release, wait for PyPI (#203)
Avoid racing the tag push that publishes to PyPI by triggering distribution via workflow_run and polling PyPI before install/hash. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d72aeca commit 4bca26f

5 files changed

Lines changed: 108 additions & 47 deletions

File tree

.github/workflows/notify-homebrew.yml

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
name: Notify Homebrew tap
22

3-
# Triggers on release tags for the CLI package. Sends a
4-
# repository_dispatch event to Create-Python-App/homebrew-tap so its
5-
# update-formula workflow can bump the formula and push it.
3+
# Runs after Release succeeds (PyPI already uploaded), or manually.
4+
# Sends repository_dispatch to Create-Python-App/homebrew-tap so
5+
# update-formula can bump the formula once the sdist is on PyPI.
66
on:
7-
push:
8-
tags:
9-
- "create-awesome-python-app@*"
7+
workflow_run:
8+
workflows: ["Release"]
9+
types: [completed]
1010
workflow_dispatch:
1111
inputs:
1212
version:
@@ -19,22 +19,51 @@ permissions:
1919
jobs:
2020
notify:
2121
name: Dispatch to homebrew-tap
22+
if: >-
23+
github.event_name == 'workflow_dispatch' ||
24+
(github.event.workflow_run.conclusion == 'success' &&
25+
startsWith(github.event.workflow_run.head_branch, 'create-awesome-python-app@'))
2226
runs-on: ubuntu-latest
2327
environment: pypi
2428
steps:
2529
- name: Resolve version
2630
id: version
2731
env:
28-
TAG_REF: ${{ github.ref_name }}
2932
INPUT_VERSION: ${{ github.event.inputs.version }}
33+
RUN_BRANCH: ${{ github.event.workflow_run.head_branch }}
3034
run: |
31-
if [ -n "$INPUT_VERSION" ]; then
35+
set -euo pipefail
36+
if [ -n "${INPUT_VERSION:-}" ]; then
3237
VERSION="$INPUT_VERSION"
3338
else
34-
VERSION="${TAG_REF#create-awesome-python-app@}"
39+
VERSION="${RUN_BRANCH#create-awesome-python-app@}"
40+
fi
41+
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.-]+)?$'; then
42+
echo "::error::Invalid Homebrew notify version: $VERSION"
43+
exit 1
3544
fi
3645
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
3746
47+
- name: Wait for PyPI package
48+
env:
49+
VERSION: ${{ steps.version.outputs.version }}
50+
PACKAGE: create-awesome-python-app
51+
run: |
52+
set -euo pipefail
53+
attempts=24
54+
delay=15
55+
for attempt in $(seq 1 "$attempts"); do
56+
if curl -sfL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" \
57+
| jq -e '.urls[] | select(.packagetype=="sdist") | .digests.sha256' >/dev/null; then
58+
echo "PyPI has ${PACKAGE}==${VERSION}"
59+
exit 0
60+
fi
61+
echo "::warning::PyPI not ready for ${PACKAGE}==${VERSION} (attempt ${attempt}/${attempts}); retrying in ${delay}s"
62+
sleep "$delay"
63+
done
64+
echo "::error::Timed out waiting for ${PACKAGE}==${VERSION} on PyPI"
65+
exit 1
66+
3867
- name: Dispatch repository event
3968
env:
4069
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}

.github/workflows/publish-aur.yml

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
name: Publish to AUR
22

3-
# Triggers on release tags for the CLI package.
3+
# Runs after Release succeeds (PyPI already uploaded), or manually.
44
# Publishes the updated PKGBUILD to aur.archlinux.org and keeps the
55
# Create-Python-App/aur-package GitHub mirror in sync.
66
on:
7-
push:
8-
tags:
9-
- "create-awesome-python-app@*"
7+
workflow_run:
8+
workflows: ["Release"]
9+
types: [completed]
1010
workflow_dispatch:
1111
inputs:
1212
version:
@@ -28,20 +28,25 @@ env:
2828
jobs:
2929
aur:
3030
name: Update AUR package
31+
if: >-
32+
github.event_name == 'workflow_dispatch' ||
33+
(github.event.workflow_run.conclusion == 'success' &&
34+
startsWith(github.event.workflow_run.head_branch, 'create-awesome-python-app@'))
3135
runs-on: ubuntu-latest
3236
environment: pypi
3337
timeout-minutes: 20
3438
steps:
3539
- name: Resolve version
3640
id: version
3741
env:
38-
TAG_REF: ${{ github.ref_name }}
3942
INPUT_VERSION: ${{ github.event.inputs.version }}
43+
RUN_BRANCH: ${{ github.event.workflow_run.head_branch }}
4044
run: |
41-
if [ -n "$INPUT_VERSION" ]; then
45+
set -euo pipefail
46+
if [ -n "${INPUT_VERSION:-}" ]; then
4247
VERSION="$INPUT_VERSION"
4348
else
44-
VERSION="${TAG_REF#create-awesome-python-app@}"
49+
VERSION="${RUN_BRANCH#create-awesome-python-app@}"
4550
fi
4651
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.-]+)?$'; then
4752
echo "::error::Invalid AUR package version: $VERSION"
@@ -82,7 +87,8 @@ jobs:
8287
sed -i "s/^pkgver=.*/pkgver=${NEW_VERSION}/" PKGBUILD
8388
sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD
8489
85-
META=$(retry 5 10 curl -sfL "https://pypi.org/pypi/${PYPI_PACKAGE}/${NEW_VERSION}/json")
90+
# PyPI CDN can lag the upload response; poll for indexing.
91+
META=$(retry 24 15 curl -sfL "https://pypi.org/pypi/${PYPI_PACKAGE}/${NEW_VERSION}/json")
8692
SHA=$(echo "$META" | jq -r '.urls[] | select(.packagetype=="sdist") | .digests.sha256')
8793
if [ -z "$SHA" ] || [ "$SHA" = "null" ]; then
8894
echo "::error::Failed to resolve PyPI sdist sha256 for v${NEW_VERSION}" >&2
@@ -133,8 +139,6 @@ jobs:
133139
retry 5 15 git ls-remote "https://aur.archlinux.org/${AUR_PACKAGE}.git" >/dev/null
134140
135141
- name: Publish to AUR
136-
# Pushes to aur.archlinux.org via SSH. The action reads the
137-
# updated PKGBUILD, regenerates .SRCINFO, and pushes.
138142
uses: ulises-jeremias/github-actions-aur-publish@217e4e2abbbee9ecc942bdc0681302e233656d9f # v1
139143
with:
140144
pkgname: create-awesome-python-app
@@ -144,8 +148,6 @@ jobs:
144148
commit_message: "Update to version ${{ steps.version.outputs.version }}"
145149
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
146150
allow_empty_commits: "false"
147-
# dsa is no longer supported in modern OpenSSH; omit it to
148-
# avoid "Unknown key type" errors during keyscan.
149151
ssh_keyscan_types: "rsa,ecdsa,ed25519"
150152

151153
- name: Verify AUR RPC after publish
@@ -193,9 +195,6 @@ jobs:
193195
} >> "$GITHUB_STEP_SUMMARY"
194196
195197
- name: Sync updated PKGBUILD to GitHub mirror
196-
# Keep the aur-package GitHub mirror in sync with what's live
197-
# on AUR. Only PKGBUILD needs to be committed here — .SRCINFO
198-
# is regenerated automatically by AUR from PKGBUILD.
199198
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
200199
with:
201200
repository: aur-package

.github/workflows/publish-docker.yml

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
name: Publish Docker image
22

3-
# Triggers on release tags for the CLI package. Version comes directly
4-
# from the tag ref (create-awesome-python-app@X.Y.Z).
3+
# Runs after Release succeeds (PyPI already uploaded), or manually to
4+
# rebuild an image for an existing version. Avoids racing the tag push
5+
# that also starts Release.
56
on:
6-
push:
7-
tags:
8-
- "create-awesome-python-app@*"
9-
# Manual trigger to rebuild an image for an existing version.
7+
workflow_run:
8+
workflows: ["Release"]
9+
types: [completed]
1010
workflow_dispatch:
1111
inputs:
1212
version:
@@ -19,6 +19,10 @@ permissions:
1919
jobs:
2020
docker:
2121
name: Build and push Docker image
22+
if: >-
23+
github.event_name == 'workflow_dispatch' ||
24+
(github.event.workflow_run.conclusion == 'success' &&
25+
startsWith(github.event.workflow_run.head_branch, 'create-awesome-python-app@'))
2226
runs-on: ubuntu-latest
2327
environment: pypi
2428
steps:
@@ -29,14 +33,18 @@ jobs:
2933
- name: Resolve version
3034
id: version
3135
env:
32-
TAG_REF: ${{ github.ref_name }}
3336
INPUT_VERSION: ${{ github.event.inputs.version }}
37+
RUN_BRANCH: ${{ github.event.workflow_run.head_branch }}
3438
run: |
35-
if [ -n "$INPUT_VERSION" ]; then
39+
set -euo pipefail
40+
if [ -n "${INPUT_VERSION:-}" ]; then
3641
VERSION="$INPUT_VERSION"
3742
else
38-
# Tag format: create-awesome-python-app@X.Y.Z
39-
VERSION="${TAG_REF#create-awesome-python-app@}"
43+
VERSION="${RUN_BRANCH#create-awesome-python-app@}"
44+
fi
45+
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.-]+)?$'; then
46+
echo "::error::Invalid Docker image version: $VERSION"
47+
exit 1
4048
fi
4149
MAJOR=$(echo "$VERSION" | cut -d. -f1)
4250
MINOR=$(echo "$VERSION" | cut -d. -f2)
@@ -46,6 +54,26 @@ jobs:
4654
echo "minor=$MINOR"
4755
} >> "$GITHUB_OUTPUT"
4856
57+
- name: Wait for PyPI package
58+
env:
59+
VERSION: ${{ steps.version.outputs.version }}
60+
PACKAGE: create-awesome-python-app
61+
run: |
62+
set -euo pipefail
63+
attempts=24
64+
delay=15
65+
for attempt in $(seq 1 "$attempts"); do
66+
if curl -sfL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" \
67+
| jq -e '.urls[] | select(.packagetype=="sdist") | .digests.sha256' >/dev/null; then
68+
echo "PyPI has ${PACKAGE}==${VERSION}"
69+
exit 0
70+
fi
71+
echo "::warning::PyPI not ready for ${PACKAGE}==${VERSION} (attempt ${attempt}/${attempts}); retrying in ${delay}s"
72+
sleep "$delay"
73+
done
74+
echo "::error::Timed out waiting for ${PACKAGE}==${VERSION} on PyPI"
75+
exit 1
76+
4977
- name: Set up QEMU
5078
# Required so buildx can cross-build linux/arm64 on the x86_64
5179
# ubuntu-latest runner.
@@ -67,9 +95,6 @@ jobs:
6795
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
6896
with:
6997
images: ulisesjeremias/create-awesome-python-app
70-
# `latest` is pushed on tag pushes AND on explicit workflow_dispatch
71-
# runs (which always execute against main, so they represent the
72-
# current release).
7398
tags: |
7499
type=raw,value=latest
75100
type=raw,value=${{ steps.version.outputs.version }}

docs/DISTRIBUTION_SETUP.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
| Channel | Workflow | Secret(s) |
66
|---------|----------|-----------|
7-
| **PyPI** | `publish.yml` | OIDC Trusted Publishing (no token) |
8-
| **Docker** | `publish-docker.yml` | `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` |
9-
| **AUR** | `publish-aur.yml` | `AUR_SSH_PRIVATE_KEY`, `AUR_REPO_TOKEN` |
10-
| **Homebrew** | `notify-homebrew.yml``homebrew-tap` | `HOMEBREW_TAP_TOKEN` |
7+
| **PyPI** | `publish.yml` (Release) | OIDC Trusted Publishing (no token) |
8+
| **Docker** | `publish-docker.yml` (after Release) | `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` |
9+
| **AUR** | `publish-aur.yml` (after Release) | `AUR_SSH_PRIVATE_KEY`, `AUR_REPO_TOKEN` |
10+
| **Homebrew** | `notify-homebrew.yml``homebrew-tap` (after Release) | `HOMEBREW_TAP_TOKEN` |
1111

1212
Configure secrets under **Settings → Environments → `pypi` → Environment secrets**
1313
(not repository Action secrets). Release, Docker, AUR, and Homebrew jobs all use
1414
`environment: pypi`.
1515

16+
Docker / AUR / Homebrew install from PyPI, so they run via `workflow_run`
17+
**after** Release succeeds (they no longer race the same tag push). Each
18+
consumer also polls PyPI with retries for CDN indexing lag.
19+
1620
## PyPI Trusted Publishing
1721

1822
The Release job uses the GitHub Actions environment **`pypi`**
@@ -37,8 +41,10 @@ git push origin create-awesome-python-app@X.Y.Z
3741

3842
Then:
3943

40-
1. Confirm Release (PyPI), Notify Homebrew, Publish to AUR, and Docker workflows
41-
2. Smoke: `uvx create-awesome-python-app@X.Y.Z --help`
44+
1. Confirm **Release** (PyPI + GitHub Release) succeeds — Docker, AUR, and
45+
Homebrew notify then start via `workflow_run`
46+
2. Confirm those three workflows complete
47+
3. Smoke: `uvx --python 3.12 create-awesome-python-app@X.Y.Z --help`
4248

4349
## Docker Hub
4450

@@ -106,8 +112,9 @@ If the workflow fails before `Publish to AUR`, check the preflight log first:
106112
environment or the workflow did not get environment access.
107113
- AUR RPC / `git ls-remote` failures are usually transient AUR availability
108114
issues; rerun the job after a few minutes.
109-
- PyPI metadata failures usually mean the release tag fired before PyPI finished
110-
indexing the sdist; rerun once PyPI shows the version.
115+
- PyPI metadata failures usually mean CDN indexing lag after upload; the
116+
workflow polls PyPI for several minutes — if it still fails, check
117+
`https://pypi.org/pypi/create-awesome-python-app/<version>/json` and rerun.
111118

112119
If `Publish to AUR` succeeds but the RPC summary still shows the previous
113120
version, wait for AUR propagation and rerun the distribution smoke workflow.
@@ -165,4 +172,5 @@ red.
165172
## After secrets are in place
166173

167174
Every subsequent release only requires tagging `create-awesome-python-app@X.Y.Z`.
168-
PyPI, Homebrew notify, AUR, and Docker workflows fan out from that tag.
175+
**Release** publishes to PyPI; Docker, AUR, and Homebrew notify follow when
176+
that workflow succeeds.

docs/VERSIONING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ After the release-prep PR is merged:
3232
2. Push the tag
3333
3. `publish.yml` builds and publishes both packages to PyPI via OIDC (see #58)
3434
4. GitHub Release notes are extracted from the matching `CHANGELOG.md` section
35-
5. Distribution workflows update Docker, Homebrew, and AUR
35+
5. After Release succeeds, Docker / Homebrew / AUR workflows run
3636

3737
```bash
3838
git tag create-awesome-python-app@X.Y.Z

0 commit comments

Comments
 (0)