From 4c361fd29e1cfa7ad7ecade07a881d259b4e16b8 Mon Sep 17 00:00:00 2001 From: ulises-jeremias Date: Fri, 17 Jul 2026 10:20:53 -0300 Subject: [PATCH] ci: run Docker/AUR/Homebrew after Release, wait for PyPI 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 --- .github/workflows/notify-homebrew.yml | 47 +++++++++++++++++++----- .github/workflows/publish-aur.yml | 29 ++++++++------- .github/workflows/publish-docker.yml | 51 ++++++++++++++++++++------- docs/DISTRIBUTION_SETUP.md | 26 +++++++++----- docs/VERSIONING.md | 2 +- 5 files changed, 108 insertions(+), 47 deletions(-) diff --git a/.github/workflows/notify-homebrew.yml b/.github/workflows/notify-homebrew.yml index b84930a..75b6573 100644 --- a/.github/workflows/notify-homebrew.yml +++ b/.github/workflows/notify-homebrew.yml @@ -1,12 +1,12 @@ name: Notify Homebrew tap -# Triggers on release tags for the CLI package. Sends a -# repository_dispatch event to Create-Python-App/homebrew-tap so its -# update-formula workflow can bump the formula and push it. +# Runs after Release succeeds (PyPI already uploaded), or manually. +# Sends repository_dispatch to Create-Python-App/homebrew-tap so +# update-formula can bump the formula once the sdist is on PyPI. on: - push: - tags: - - "create-awesome-python-app@*" + workflow_run: + workflows: ["Release"] + types: [completed] workflow_dispatch: inputs: version: @@ -19,22 +19,51 @@ permissions: jobs: notify: name: Dispatch to homebrew-tap + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'create-awesome-python-app@')) runs-on: ubuntu-latest environment: pypi steps: - name: Resolve version id: version env: - TAG_REF: ${{ github.ref_name }} INPUT_VERSION: ${{ github.event.inputs.version }} + RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | - if [ -n "$INPUT_VERSION" ]; then + set -euo pipefail + if [ -n "${INPUT_VERSION:-}" ]; then VERSION="$INPUT_VERSION" else - VERSION="${TAG_REF#create-awesome-python-app@}" + VERSION="${RUN_BRANCH#create-awesome-python-app@}" + fi + if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.-]+)?$'; then + echo "::error::Invalid Homebrew notify version: $VERSION" + exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Wait for PyPI package + env: + VERSION: ${{ steps.version.outputs.version }} + PACKAGE: create-awesome-python-app + run: | + set -euo pipefail + attempts=24 + delay=15 + for attempt in $(seq 1 "$attempts"); do + if curl -sfL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" \ + | jq -e '.urls[] | select(.packagetype=="sdist") | .digests.sha256' >/dev/null; then + echo "PyPI has ${PACKAGE}==${VERSION}" + exit 0 + fi + echo "::warning::PyPI not ready for ${PACKAGE}==${VERSION} (attempt ${attempt}/${attempts}); retrying in ${delay}s" + sleep "$delay" + done + echo "::error::Timed out waiting for ${PACKAGE}==${VERSION} on PyPI" + exit 1 + - name: Dispatch repository event env: GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index 5689e93..065d2a2 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -1,12 +1,12 @@ name: Publish to AUR -# Triggers on release tags for the CLI package. +# Runs after Release succeeds (PyPI already uploaded), or manually. # Publishes the updated PKGBUILD to aur.archlinux.org and keeps the # Create-Python-App/aur-package GitHub mirror in sync. on: - push: - tags: - - "create-awesome-python-app@*" + workflow_run: + workflows: ["Release"] + types: [completed] workflow_dispatch: inputs: version: @@ -28,6 +28,10 @@ env: jobs: aur: name: Update AUR package + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'create-awesome-python-app@')) runs-on: ubuntu-latest environment: pypi timeout-minutes: 20 @@ -35,13 +39,14 @@ jobs: - name: Resolve version id: version env: - TAG_REF: ${{ github.ref_name }} INPUT_VERSION: ${{ github.event.inputs.version }} + RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | - if [ -n "$INPUT_VERSION" ]; then + set -euo pipefail + if [ -n "${INPUT_VERSION:-}" ]; then VERSION="$INPUT_VERSION" else - VERSION="${TAG_REF#create-awesome-python-app@}" + VERSION="${RUN_BRANCH#create-awesome-python-app@}" fi if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.-]+)?$'; then echo "::error::Invalid AUR package version: $VERSION" @@ -82,7 +87,8 @@ jobs: sed -i "s/^pkgver=.*/pkgver=${NEW_VERSION}/" PKGBUILD sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD - META=$(retry 5 10 curl -sfL "https://pypi.org/pypi/${PYPI_PACKAGE}/${NEW_VERSION}/json") + # PyPI CDN can lag the upload response; poll for indexing. + META=$(retry 24 15 curl -sfL "https://pypi.org/pypi/${PYPI_PACKAGE}/${NEW_VERSION}/json") SHA=$(echo "$META" | jq -r '.urls[] | select(.packagetype=="sdist") | .digests.sha256') if [ -z "$SHA" ] || [ "$SHA" = "null" ]; then echo "::error::Failed to resolve PyPI sdist sha256 for v${NEW_VERSION}" >&2 @@ -133,8 +139,6 @@ jobs: retry 5 15 git ls-remote "https://aur.archlinux.org/${AUR_PACKAGE}.git" >/dev/null - name: Publish to AUR - # Pushes to aur.archlinux.org via SSH. The action reads the - # updated PKGBUILD, regenerates .SRCINFO, and pushes. uses: ulises-jeremias/github-actions-aur-publish@217e4e2abbbee9ecc942bdc0681302e233656d9f # v1 with: pkgname: create-awesome-python-app @@ -144,8 +148,6 @@ jobs: commit_message: "Update to version ${{ steps.version.outputs.version }}" ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} allow_empty_commits: "false" - # dsa is no longer supported in modern OpenSSH; omit it to - # avoid "Unknown key type" errors during keyscan. ssh_keyscan_types: "rsa,ecdsa,ed25519" - name: Verify AUR RPC after publish @@ -193,9 +195,6 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Sync updated PKGBUILD to GitHub mirror - # Keep the aur-package GitHub mirror in sync with what's live - # on AUR. Only PKGBUILD needs to be committed here — .SRCINFO - # is regenerated automatically by AUR from PKGBUILD. uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 with: repository: aur-package diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 541034d..44cc251 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -1,12 +1,12 @@ name: Publish Docker image -# Triggers on release tags for the CLI package. Version comes directly -# from the tag ref (create-awesome-python-app@X.Y.Z). +# Runs after Release succeeds (PyPI already uploaded), or manually to +# rebuild an image for an existing version. Avoids racing the tag push +# that also starts Release. on: - push: - tags: - - "create-awesome-python-app@*" - # Manual trigger to rebuild an image for an existing version. + workflow_run: + workflows: ["Release"] + types: [completed] workflow_dispatch: inputs: version: @@ -19,6 +19,10 @@ permissions: jobs: docker: name: Build and push Docker image + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'create-awesome-python-app@')) runs-on: ubuntu-latest environment: pypi steps: @@ -29,14 +33,18 @@ jobs: - name: Resolve version id: version env: - TAG_REF: ${{ github.ref_name }} INPUT_VERSION: ${{ github.event.inputs.version }} + RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | - if [ -n "$INPUT_VERSION" ]; then + set -euo pipefail + if [ -n "${INPUT_VERSION:-}" ]; then VERSION="$INPUT_VERSION" else - # Tag format: create-awesome-python-app@X.Y.Z - VERSION="${TAG_REF#create-awesome-python-app@}" + VERSION="${RUN_BRANCH#create-awesome-python-app@}" + fi + if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.-]+)?$'; then + echo "::error::Invalid Docker image version: $VERSION" + exit 1 fi MAJOR=$(echo "$VERSION" | cut -d. -f1) MINOR=$(echo "$VERSION" | cut -d. -f2) @@ -46,6 +54,26 @@ jobs: echo "minor=$MINOR" } >> "$GITHUB_OUTPUT" + - name: Wait for PyPI package + env: + VERSION: ${{ steps.version.outputs.version }} + PACKAGE: create-awesome-python-app + run: | + set -euo pipefail + attempts=24 + delay=15 + for attempt in $(seq 1 "$attempts"); do + if curl -sfL "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" \ + | jq -e '.urls[] | select(.packagetype=="sdist") | .digests.sha256' >/dev/null; then + echo "PyPI has ${PACKAGE}==${VERSION}" + exit 0 + fi + echo "::warning::PyPI not ready for ${PACKAGE}==${VERSION} (attempt ${attempt}/${attempts}); retrying in ${delay}s" + sleep "$delay" + done + echo "::error::Timed out waiting for ${PACKAGE}==${VERSION} on PyPI" + exit 1 + - name: Set up QEMU # Required so buildx can cross-build linux/arm64 on the x86_64 # ubuntu-latest runner. @@ -67,9 +95,6 @@ jobs: uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ulisesjeremias/create-awesome-python-app - # `latest` is pushed on tag pushes AND on explicit workflow_dispatch - # runs (which always execute against main, so they represent the - # current release). tags: | type=raw,value=latest type=raw,value=${{ steps.version.outputs.version }} diff --git a/docs/DISTRIBUTION_SETUP.md b/docs/DISTRIBUTION_SETUP.md index 5283500..702cd7b 100644 --- a/docs/DISTRIBUTION_SETUP.md +++ b/docs/DISTRIBUTION_SETUP.md @@ -4,15 +4,19 @@ | Channel | Workflow | Secret(s) | |---------|----------|-----------| -| **PyPI** | `publish.yml` | OIDC Trusted Publishing (no token) | -| **Docker** | `publish-docker.yml` | `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` | -| **AUR** | `publish-aur.yml` | `AUR_SSH_PRIVATE_KEY`, `AUR_REPO_TOKEN` | -| **Homebrew** | `notify-homebrew.yml` → `homebrew-tap` | `HOMEBREW_TAP_TOKEN` | +| **PyPI** | `publish.yml` (Release) | OIDC Trusted Publishing (no token) | +| **Docker** | `publish-docker.yml` (after Release) | `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` | +| **AUR** | `publish-aur.yml` (after Release) | `AUR_SSH_PRIVATE_KEY`, `AUR_REPO_TOKEN` | +| **Homebrew** | `notify-homebrew.yml` → `homebrew-tap` (after Release) | `HOMEBREW_TAP_TOKEN` | Configure secrets under **Settings → Environments → `pypi` → Environment secrets** (not repository Action secrets). Release, Docker, AUR, and Homebrew jobs all use `environment: pypi`. +Docker / AUR / Homebrew install from PyPI, so they run via `workflow_run` +**after** Release succeeds (they no longer race the same tag push). Each +consumer also polls PyPI with retries for CDN indexing lag. + ## PyPI Trusted Publishing The Release job uses the GitHub Actions environment **`pypi`** @@ -37,8 +41,10 @@ git push origin create-awesome-python-app@X.Y.Z Then: -1. Confirm Release (PyPI), Notify Homebrew, Publish to AUR, and Docker workflows -2. Smoke: `uvx create-awesome-python-app@X.Y.Z --help` +1. Confirm **Release** (PyPI + GitHub Release) succeeds — Docker, AUR, and + Homebrew notify then start via `workflow_run` +2. Confirm those three workflows complete +3. Smoke: `uvx --python 3.12 create-awesome-python-app@X.Y.Z --help` ## Docker Hub @@ -106,8 +112,9 @@ If the workflow fails before `Publish to AUR`, check the preflight log first: environment or the workflow did not get environment access. - AUR RPC / `git ls-remote` failures are usually transient AUR availability issues; rerun the job after a few minutes. -- PyPI metadata failures usually mean the release tag fired before PyPI finished - indexing the sdist; rerun once PyPI shows the version. +- PyPI metadata failures usually mean CDN indexing lag after upload; the + workflow polls PyPI for several minutes — if it still fails, check + `https://pypi.org/pypi/create-awesome-python-app//json` and rerun. If `Publish to AUR` succeeds but the RPC summary still shows the previous version, wait for AUR propagation and rerun the distribution smoke workflow. @@ -165,4 +172,5 @@ red. ## After secrets are in place Every subsequent release only requires tagging `create-awesome-python-app@X.Y.Z`. -PyPI, Homebrew notify, AUR, and Docker workflows fan out from that tag. +**Release** publishes to PyPI; Docker, AUR, and Homebrew notify follow when +that workflow succeeds. diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 52fa752..6aecc77 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -32,7 +32,7 @@ After the release-prep PR is merged: 2. Push the tag 3. `publish.yml` builds and publishes both packages to PyPI via OIDC (see #58) 4. GitHub Release notes are extracted from the matching `CHANGELOG.md` section -5. Distribution workflows update Docker, Homebrew, and AUR +5. After Release succeeds, Docker / Homebrew / AUR workflows run ```bash git tag create-awesome-python-app@X.Y.Z