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
35 changes: 29 additions & 6 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,39 @@ buf curl --protocol grpc --http2-prior-knowledge \

## Releases

We support the current minor release and the two before it (for example, `v1.0`, `v0.9`, and `v0.8`). Each minor
version has a `releases/vX.Y.x` branch, and patches for that line are cut from it. New minor and major releases are cut
from `main`.

Releases are cut from the [release workflow](workflows/release.yaml) via manual dispatch; you'll need write access to
the repository. The workflow computes the version, creates and pushes the tag, then runs goreleaser to publish the
binaries, container image, and GitHub release.

To cut a release, open **Actions -> release -> Run workflow** in the GitHub UI, select `main` as the branch, and choose
either:
To cut a release, open **Actions -> release -> Run workflow** in the GitHub UI, pick the branch, and choose either:

- a **bump** (`patch`, `minor`, or `major`); the next version is computed from the latest tag with `svu`, or
- a **bump**; the next version is computed from the latest tag with `svu`. Use `minor` or `major` from `main`, and
`patch` from a release branch (scoped to that line's tags), or
- an explicit **version** (with or without a leading `v`, e.g. `1.4.0` or `v0.2.0-alpha.20260722`), which overrides the
bump.
bump. On a release branch it must belong to that line.

Releasing `vX.Y.0` from `main` also creates the `releases/vX.Y.x` branch from the new tag.

The release is marked latest on GitHub, and the container image is tagged `latest`, only when it is the highest stable
version. A patch to an older line (say `v0.9.1` after `v1.0.1`) is published without touching `latest`.

The run fails fast, without creating a tag, if it is dispatched from a branch other than `main` or `releases/vX.Y.x`,
if the bump doesn't match the branch, if an explicit version is not valid semver or doesn't belong to the release
branch, or if the resolved tag already exists. Only one release runs at a time.

### Backporting fixes

Fixes usually land on `main` first. To ship one on a supported line, open a PR against its `releases/vX.Y.x` branch:

- If the fix applies cleanly, cherry-pick it with `git cherry-pick -x <sha>`.
- If it doesn't (for example, the code it touches doesn't exist on that line), adapt it by hand.

Either way, link the corresponding `main` commit in the PR description. A fix that only applies to an older line is
fine too; say why there's no `main` counterpart. Once merged, run the release workflow from the release branch with a
`patch` bump.

The run fails fast, without creating a tag, if it is dispatched from a branch other than `main`, if an explicit version
is not valid semver, or if the resolved tag already exists.
Release branches for lines that fall out of the support window can be deleted by hand.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ on:
pull_request:
branches:
- main
- releases/**
push:
branches:
- main
- releases/**

permissions:
contents: read
Expand Down
76 changes: 71 additions & 5 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,42 @@ on:
permissions:
contents: write

# One release at a time across all branches. Whether a release is marked latest
# depends on every other tag, so two lines releasing at once could both claim it.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
steps:
# main cuts minor and major releases; releases/vX.Y.x cuts patches for that
# line. line is empty on main and vX.Y on a release branch.
- name: Guard branch
if: github.ref != 'refs/heads/main'
id: line
env:
REF: ${{ github.ref }}
run: |
echo "::error::Releases can only be run from main (got ${REF})"
exit 1
set -euo pipefail
case "${REF}" in
refs/heads/main)
line=""
;;
refs/heads/releases/v*.x)
line="${REF#refs/heads/releases/}"
line="${line%.x}"
if ! echo "${line}" | grep -Eq '^v[0-9]+\.[0-9]+$'; then
echo "::error::Release branches must be named releases/vX.Y.x (got ${REF})"
exit 1
fi
;;
*)
echo "::error::Releases can only be run from main or releases/vX.Y.x (got ${REF})"
exit 1
;;
esac
echo "line=${line}" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -46,6 +67,7 @@ jobs:
env:
INPUT_VERSION: ${{ inputs.version }}
INPUT_BUMP: ${{ inputs.bump }}
LINE: ${{ steps.line.outputs.line }}
run: |
set -euo pipefail
if [ -n "${INPUT_VERSION}" ]; then
Expand All @@ -54,7 +76,22 @@ jobs:
echo "::error::Invalid version '${INPUT_VERSION}'. Expected semver like 1.2.3 or v1.2.3-rc1 (build metadata '+...' is not supported)"
exit 1
fi
if [ -n "${LINE}" ] && [ "${tag#"${LINE}".}" = "${tag}" ]; then
echo "::error::Version ${tag} does not belong on releases/${LINE}.x"
exit 1
fi
elif [ -n "${LINE}" ]; then
if [ "${INPUT_BUMP}" != "patch" ]; then
echo "::error::Release branches only cut patch releases (got '${INPUT_BUMP}'); run minor and major bumps from main"
exit 1
fi
# svu picks the highest tag in the repo, so scope it to this line.
tag="$(svu patch --tag.pattern "${LINE}.*")"
else
if [ "${INPUT_BUMP}" = "patch" ]; then
echo "::error::Patch releases are cut from their releases/vX.Y.x branch, not main"
exit 1
fi
tag="$(svu "${INPUT_BUMP}")"
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
Expand All @@ -80,6 +117,35 @@ jobs:
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"

- name: Create release branch
if: steps.line.outputs.line == ''
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
if ! echo "${TAG}" | grep -Eq '^v[0-9]+\.[0-9]+\.0$'; then
exit 0
fi
branch="releases/${TAG%.0}.x"
git push origin "${TAG}^{commit}:refs/heads/${branch}"
echo "Created ${branch} from ${TAG}"

# Only the highest stable version is latest, so a patch to an older line
# doesn't take latest from a newer one. goreleaser reads MAKE_LATEST.
- name: Resolve latest
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
highest="$(git tag -l 'v*' --sort=-v:refname | grep -v -- '-' | head -n1)"
if [ "${TAG}" = "${highest}" ]; then
latest=true
else
latest=false
fi
echo "MAKE_LATEST=${latest}" >> "$GITHUB_ENV"
echo "Latest: ${latest} (highest stable tag is ${highest})"

- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
Expand Down
4 changes: 3 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dockers_v2:
tags:
- "v{{.Version }}"
- "{{ if .IsNightly }}nightly{{ end }}"
- "{{ if not .IsNightly }}latest{{ end }}"
- '{{ if eq (envOrDefault "MAKE_LATEST" "false") "true" }}latest{{ end }}'

labels:
"org.opencontainers.image.title": "{{ .ProjectName }}"
Expand Down Expand Up @@ -79,6 +79,8 @@ release:
name: temporal-proxy
draft: false
prerelease: auto
# Set by the release workflow; only the highest stable version is latest.
make_latest: '{{ envOrDefault "MAKE_LATEST" "false" }}'
name_template: "{{ .Version }} Release"
header: |
## Temporal Proxy {{ .Version }}
Expand Down
Loading