Skip to content
Merged
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
47 changes: 38 additions & 9 deletions .github/workflows/notify-homebrew.yml
Original file line numberDiff line numberDiff line change
@@ -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:
Expand All@@ -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 }}
Expand Down
29 changes: 14 additions & 15 deletions .github/workflows/publish-aur.yml
Original file line numberDiff line numberDiff line change
@@ -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:
Expand All@@ -28,20 +28,25 @@ 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
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 AUR package version: $VERSION"
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand DownExpand Up@@ -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
Expand Down
51 changes: 38 additions & 13 deletions .github/workflows/publish-docker.yml
Original file line numberDiff line numberDiff line change
@@ -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:
Expand All@@ -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:
Expand All@@ -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)
Expand All@@ -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.
Expand All@@ -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 }}
Expand Down
26 changes: 17 additions & 9 deletions docs/DISTRIBUTION_SETUP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`**
Expand All@@ -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

Expand DownExpand Up@@ -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/<version>/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.
Expand DownExpand Up@@ -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.
2 changes: 1 addition & 1 deletion docs/VERSIONING.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
Loading