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
17 changes: 15 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,24 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# **Tagged by commit SHA only. `:latest` belongs to release.yml.**
#
# Both workflows used to push `:latest`, with no concurrency group
# between them, so a release raced the `main` push that carried it.
# v1.0.2 lost that race: `think-watch-server:latest` ended up
# pointing at a main-branch build rather than the tagged release,
# while `think-watch-web:latest` stayed correct only because this
# workflow's web job happened to be cancelled mid-run.
#
# `:latest` means "the newest stable release", which is a fact only
# a tag knows. A branch push cannot know it, so it must not claim
# it. The SHA tag stays — that is how you deploy an unreleased
# `main` on purpose, and it can never collide with a release tag.
- name: Create and push multi-arch manifest
working-directory: ${{ runner.temp }}/digests
run: |
IMAGE="${IMAGE_PREFIX}/think-watch-server"
docker buildx imagetools create \
-t "${IMAGE}:latest" \
-t "${IMAGE}:${{ github.sha }}" \
$(printf "${IMAGE}@sha256:%s " *)

Expand Down Expand Up @@ -250,8 +262,9 @@ jobs:
file: deploy/docker/Dockerfile.web
push: true
platforms: linux/amd64,linux/arm64
# SHA only — see the note on the server manifest job above.
# `:latest` is release.yml's to set.
tags: |
${{ env.IMAGE_PREFIX }}/think-watch-web:latest
${{ env.IMAGE_PREFIX }}/think-watch-web:${{ github.sha }}
cache-from: type=gha,scope=web
cache-to: type=gha,scope=web,mode=max
20 changes: 20 additions & 0 deletions docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ page.

Total wall-clock: ~13 min for a typical release.

### Who owns which image tag

| Tag | Set by | Means |
|---|---|---|
| `:X.Y.Z` | `release.yml` (tag push) | That release, exactly |
| `:latest` | `release.yml`, stable releases only | The newest stable release |
| `:<commit sha>` | `ci.yml` (push to `main`) | That commit on `main`, released or not |

**`ci.yml` must never push `:latest`.** Both workflows used to, with no
concurrency group between them, so cutting a release raced the `main`
push that carried it — two runs, same tag, last writer wins. v1.0.2 lost
that race: `think-watch-server:latest` pointed at a `main` build instead
of the tagged release, and `think-watch-web:latest` was correct only
because CI's web job happened to be cancelled that run. The mismatch is
invisible from the Release page, which looks entirely healthy.

A branch push cannot know which commit is the newest stable release, so
it must not claim the tag that asserts it. To deploy an unreleased
`main`, pull it by commit SHA on purpose.

## Pre-release tags

For `1.0.0-rc.1`, `1.1.0-beta.2`, `2.0.0-alpha.5`:
Expand Down
Loading