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
32 changes: 16 additions & 16 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
name: Build and publish Docker image
name: Manually rebuild and publish a release

# Fires only on a vX.Y.Z tag -- which only ever exists once release-please
# (see release-please.yml) has cut an actual release by merging its release
# PR. A plain push to main/develop never reaches this workflow at all, so
# the multi-arch build only ever runs for a real release.
# Manual fallback only. The normal path is the `build` job inside
# release-please.yml, which runs automatically in the same workflow run as
# the release -- see that file's header comment for why this can't instead
# be a plain `on: push: tags:` workflow (GITHUB_TOKEN-created tags don't
# trigger other workflow runs). Use this to re-publish an existing vX.Y.Z
# release on demand, e.g. after a GHCR outage or a manual retry.

on:
push:
tags: ["v*.*.*"]
workflow_dispatch:
inputs:
tag:
description: "Existing vX.Y.Z tag to rebuild and publish"
required: true
type: string

permissions:
contents: read
Expand All @@ -26,6 +31,8 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}

- name: Set up QEMU (for linux/arm64)
uses: docker/setup-qemu-action@v3
Expand All @@ -40,23 +47,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# docker/metadata-action reads the semver straight off the triggering
# tag ref (github.ref), so no manual version wiring is needed here --
# unlike the previous single-workflow setup where the build job had to
# check out a tag computed by an earlier job in the same run.
- name: Extract image metadata (tags + OCI labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ inputs.tag }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag }}
type=raw,value=latest

# Build always runs as a "does the Dockerfile still build (and pass
# its embedded test stage)?" check; push is unconditional here since
# this workflow itself only ever runs for a real vX.Y.Z tag.
- name: Build and push image
uses: docker/build-push-action@v6
with:
Expand Down
69 changes: 67 additions & 2 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ name: Release Please
# accumulates Conventional Commits into the next semantic version +
# CHANGELOG.md entry. Nothing is tagged or released automatically -- only
# merging that PR (a normal, reviewable PR merge, not a bot pushing straight
# to main) makes release-please cut the vX.Y.Z tag + GitHub Release. That
# tag push is what publish.yml reacts to.
# to main) makes release-please cut the vX.Y.Z tag + GitHub Release.
#
# The Docker build/publish job below runs in the SAME workflow run,
# triggered off release-please's own step outputs -- NOT off a separate
# `on: push: tags:` workflow. GitHub Actions deliberately does not let a ref
# (tag/branch) created using the default GITHUB_TOKEN trigger further
# workflow runs (recursion guard), and that's exactly how release-please
# creates its tag, so a second workflow listening for that tag push would
# never fire. Continuing in this same run sidesteps that entirely.

on:
push:
Expand All @@ -15,11 +22,69 @@ permissions:
contents: write
pull-requests: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
release-please:
runs-on: ubuntu-latest
outputs:
# release-please-action's output naming has changed across versions
# (bare vs. "<path>--"-prefixed once a config/manifest file is used);
# the fallback covers whichever this action version actually emits.
release_created: ${{ steps.release.outputs.release_created || steps.release.outputs['.--release_created'] }}
tag_name: ${{ steps.release.outputs.tag_name || steps.release.outputs['.--tag_name'] }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

build:
name: Build and push image
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release-please.outputs.tag_name }}

- name: Set up QEMU (for linux/arm64)
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract image metadata (tags + OCI labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}},value=${{ needs.release-please.outputs.tag_name }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.tag_name }}
type=raw,value=latest

- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
23 changes: 14 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,22 @@ the test stage, never test files themselves). `HEALTHCHECK` hits `/healthz` with
### Branching, versioning & release pipeline

`develop` is the integration branch (feature branches β†’ PR β†’ `develop`); `main` only moves via a
PR from `develop`. Three separate workflows split the concerns:
PR from `develop`.

- `.github/workflows/ci.yml` β€” the `test` job, on every push (`main`/`develop`) and every PR.
- `.github/workflows/release-please.yml` β€” runs
[Release Please](https://github.com/googleapis/release-please) (config:
`release-please-config.json` + `.release-please-manifest.json`) on every push to `main`. It never
pushes to `main` directly: it maintains a standing release PR (version bump + `CHANGELOG.md`)
from Conventional Commits (`fix:`, `feat:`, `BREAKING CHANGE:`) and only cuts the `vX.Y.Z` tag +
GitHub Release once that PR is actually merged.
- `.github/workflows/publish.yml` β€” triggers only on a `vX.Y.Z` tag push (i.e. only once
release-please has cut a release) and does the multi-arch Docker build + push to GHCR.
- `.github/workflows/release-please.yml` β€” runs on every push to `main`. Its `release-please` job
runs [Release Please](https://github.com/googleapis/release-please) (config:
`release-please-config.json` + `.release-please-manifest.json`), which never pushes to `main`
directly: it maintains a standing release PR (version bump + `CHANGELOG.md`) from Conventional
Commits (`fix:`, `feat:`, `BREAKING CHANGE:`) and only cuts the `vX.Y.Z` tag + GitHub Release once
that PR is merged. The workflow's `build` job then runs the multi-arch Docker build + GHCR push,
gated on `release-please`'s `release_created` output, **in that same run** β€” it is deliberately
*not* a separate `on: push: tags:` workflow, because GitHub Actions won't let a tag created via
the default `GITHUB_TOKEN` (which is how release-please creates it) trigger another workflow run;
a separate publish workflow listening for that tag push would simply never fire. (Learned this the
hard way: the first release-please rollout cut `v0.2.0` with no Docker image to show for it.)
- `.github/workflows/publish.yml` β€” manual-only (`workflow_dispatch`) fallback to rebuild/republish
an already-tagged release on demand.

The version lives in `waypoint/__init__.py` (`__version__`, marked with a trailing
`# x-release-please-version` comment that release-please's generic file updater matches on) β€” don't
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,13 @@ and the web routes (via Flask's test client) β€” all without needing a real IMAP
keeps a standing **release PR** up to date with the accumulated version bump and `CHANGELOG.md`
entry β€” it never pushes to `main` directly.
- Merging that release PR (like any other PR, via review) is what actually cuts the `vX.Y.Z` tag +
GitHub Release. Only that tag push triggers `.github/workflows/publish.yml`, which builds the
multi-arch image and publishes it to GHCR β€” a plain commit landing on `main` never does.
GitHub Release β€” and, in the same workflow run, builds the multi-arch image and publishes it to
GHCR. (The build has to live in that same run rather than a separate `on: push: tags:` workflow:
GitHub Actions doesn't let a tag created via the default `GITHUB_TOKEN` β€” which is how
release-please creates it β€” trigger a second workflow run.) A plain commit landing on `main`
never builds or publishes anything.
- `.github/workflows/publish.yml` is a manual-only fallback (`workflow_dispatch`) for re-publishing
an existing tag on demand, e.g. after a registry outage.
- Commit messages need a Conventional Commits prefix for Release Please to pick them up correctly:
`fix:` β†’ patch, `feat:` β†’ minor, `BREAKING CHANGE:` (in the footer) β†’ major. Anything else
(`chore:`, `docs:`, `test:`, ...) is included in the changelog but doesn't bump the version.
Expand Down
Loading