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
248 changes: 124 additions & 124 deletions .github/workflows/auto-release.yaml
Original file line number Diff line number Diff line change
@@ -1,146 +1,146 @@
---
# yamllint disable rule:truthy
name: Auto Release
# Push-based release tagger + release-page publisher.
#
# Runs on push to main (normal releases) and to release-* branches (backports
# / maintenance releases). Inspects conventional commits reachable from the
# pushed ref since the latest reachable v*.*.* tag via git-cliff, and:
#
# 1. creates a new vX.Y.Z git tag if a bump is warranted
# 2. creates the matching GitHub Release with notes generated by git-cliff
#
# The tag push then triggers downstream publishing (for chart/service/CLI
# repos, this is the CircleCI architect pipeline firing on the same /^v.*/
# tag filter — go-build + push-to-registries + push-to-app-catalog +
# upload-release-assets as appropriate for the repo type). Architect appends
# any binary artifacts to the release we just created here.
#
# No release PR. No human approval step. The mechanism that protects against
# bad releases is the pre-merge CI on the feature PRs that landed these
# commits — once they're on the branch, they ship.
name: Auto-release

on:
pull_request:
types: [closed]
push:
branches:
- main
- 'release-*' # maintenance branches for backports, e.g. release-2.x

permissions:
contents: write # Needed to create releases and tags
pull-requests: read # Needed to read PR information
contents: write # push tags and create the GitHub Release
# git-cliff's GitHub API lookups (commit.remote.pr_number / username
# in release notes) need PR read access via GITHUB_TOKEN.
pull-requests: read

# Scope the concurrency lock per branch so a backport tag on release-2.x
# doesn't block (or get blocked by) a main release, and vice versa. A second
# push to the same branch while a tag is being computed still waits.
concurrency:
group: auto-release-${{ github.ref_name }}
cancel-in-progress: false

jobs:
auto_release:
name: Auto Release
if: >-
github.event.pull_request.merged &&
!startsWith(github.event.pull_request.title, 'Release v')
runs-on: ubuntu-latest
tag:
name: Tag
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch all history and tags for version calculation and changelog
fetch-depth: 0
# Use PAT to allow pushing tags for commits with workflow changes
# GITHUB_TOKEN cannot push tags that modify .github/workflows/
token: ${{ secrets.RELEASE_PAT }}

# yamllint disable rule:line-length
- name: Check if commit already tagged
id: check_tag
run: |
# Check if HEAD already has a version tag
# This prevents duplicate releases on workflow re-run
EXISTING_TAG=$(git tag --points-at HEAD \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true)
if [[ -n "$EXISTING_TAG" ]]; then
echo "Commit already has version tag: $EXISTING_TAG"
echo "Skipping release to prevent duplicate tags"
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "existing_tag=$EXISTING_TAG" >> "$GITHUB_OUTPUT"
else
echo "No existing version tag found, proceeding"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
fetch-depth: 0 # full history so git-cliff sees every commit
fetch-tags: true # ensure tags are present (shallow runners drop them by default)

- name: Report skipped release
if: steps.check_tag.outputs.skip == 'true'
run: |
TAG="${{ steps.check_tag.outputs.existing_tag }}"
echo "::notice::Release skipped - already tagged as $TAG"
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "Commit already has version tag \`$TAG\`." \
>> $GITHUB_STEP_SUMMARY
echo "Re-running on an already-tagged commit is safe." \
>> $GITHUB_STEP_SUMMARY
# yamllint enable rule:line-length

- name: Set up Go
if: steps.check_tag.outputs.skip != 'true'
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
# Install git-cliff once and call it from shell so we can do all the
# expensive work (commits walk + GitHub API PR lookups) in a single
# invocation, then cheaply re-render from the cached JSON context.
# Replaces two separate orhun/git-cliff-action invocations that each
# re-installed git-cliff and re-paid the API-lookup cost.
- name: Install git-cliff
uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0
with:
go-version: '1.26' # Specify Go version
cache: true # Cache Go modules and build cache
binary: git-cliff
version: "2.13.1"
# yamllint disable-line rule:line-length
download_url: 'https://github.com/orhun/git-cliff/releases/download/v${version}/git-cliff-${version}-x86_64-unknown-linux-gnu.tar.gz'
tarball_binary_path: 'git-cliff-${version}/git-cliff'
smoke_test: '${binary} --version'

- name: Install GoReleaser
if: steps.check_tag.outputs.skip != 'true'
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
version: "~> v2"
args: --version # Print version for verification

- name: Configure Git
if: steps.check_tag.outputs.skip != 'true'
- name: Compute next version and render release notes
id: cliff
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
set -euo pipefail
# Single expensive call: walks commits, queries the GitHub API for
# per-commit PR lookups, emits the JSON context for ONLY the bumped
# release.
#
# Flag choice matters here. Both `--latest` and `--unreleased`
# *look* like single-release filters, but only one composes with
# `--bump`:
# --latest --bump : returns the latest EXISTING tag — drops the
# bump silently. We don't want this (it caused
# #15 to never produce a new tag).
# --unreleased --bump : filters to commits that aren't in any tag
# yet, computes the bumped version from
# them, returns JSON for that bumped
# release only. This is what we want.
#
# Without either filter, --bump --context returns the full release
# history in JSON and the render below would concatenate every
# past release into each new release's notes (the #13 → #15 bug).
git-cliff --unreleased --bump --context > cliff-context.json
NEXT=$(jq -r '.[0].version // empty' cliff-context.json)
echo "next computed: ${NEXT:-<empty>}"
echo "version=${NEXT}" >> "$GITHUB_OUTPUT"
# Cheap render from the cached context — no API calls, no git walk.
# `--strip all` drops header/footer; body-only is what gh release
# create --notes-file expects.
git-cliff --from-context cliff-context.json --strip all --output release-notes.md

- name: Determine Next Version
if: steps.check_tag.outputs.skip != 'true'
id: version
- name: Decide whether to tag
id: decide
env:
NEXT: ${{ steps.cliff.outputs.version }}
run: |
set -e
# Use version-aware sorting to find the semantically highest tag,
# not just the nearest reachable one. git describe can pick the
# wrong tag when multiple tags point to the same commit.
LATEST_TAG=$(git tag --sort=-version:refname | head -1)
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="v0.0.0"
set -euo pipefail
# `git describe --tags --abbrev=0` returns the closest tag reachable
# from HEAD — NOT the highest tag in the repo overall. This matters
# for backports: on release-2.x, HEAD's reachable history terminates
# before v3.0.0 was tagged, so describe correctly returns v2.3.5
# (the baseline of the 2.x line) and we end up tagging v2.3.6 rather
# than something nonsensical relative to v3.0.0.
last=$(git describe --tags --abbrev=0 --match='v*.*.*' 2>/dev/null || echo "")
echo "branch: ${GITHUB_REF_NAME}"
echo "last reachable tag: ${last:-<none>}"
if [ -z "${NEXT}" ] || [ "${NEXT}" = "${last}" ]; then
echo "No releasable commits since ${last:-inception}; skipping tag."
echo "tag=" >> "$GITHUB_OUTPUT"
else
echo "tag=${NEXT}" >> "$GITHUB_OUTPUT"
fi
# Remove 'v' prefix
LATEST_VERSION=${LATEST_TAG#v}
# Split into major, minor, patch
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_VERSION"
# Increment patch version
NEXT_PATCH=$((VERSION_PARTS[2] + 1))
# Construct next version string
NEXT_VERSION="v${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.$NEXT_PATCH"
echo "Latest tag: $LATEST_TAG"
echo "Next version: $NEXT_VERSION"
# Set output for subsequent steps
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"

- name: Create Tag
if: steps.check_tag.outputs.skip != 'true'
run: |
NEXT_VERSION="${{ steps.version.outputs.next_version }}"
git tag "$NEXT_VERSION"
git push origin "$NEXT_VERSION"
echo "Created and pushed tag $NEXT_VERSION"

- name: Run GoReleaser
if: steps.check_tag.outputs.skip != 'true'
continue-on-error: true
id: goreleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
# Create the tag AND the GitHub Release in one atomic API call. GitHub's
# release-create endpoint accepts a tag_name + target_commitish and, if
# the tag doesn't already exist, creates it pointing at that commitish
# as part of the same operation. Either both exist or neither does —
# no risk of a tag-without-release "stuck" state, no race window with
# CircleCI's `upload-release-assets` job that starts polling the
# release the instant it sees the tag.
#
# Trade-off: the tag created this way is **lightweight** (just a ref),
# not annotated. For our use — git describe, architect's tag filter,
# gitsemver, gh release view — both behave identically. Nothing in the
# giantswarm stack distinguishes them.
- name: Create release (and the tag, atomically)
if: steps.decide.outputs.tag != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}

- name: Check release result
if: steps.check_tag.outputs.skip != 'true'
env:
TAG: ${{ steps.version.outputs.next_version }}
GR_OUTCOME: ${{ steps.goreleaser.outcome }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ steps.decide.outputs.tag }}
run: |
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
> /dev/null 2>&1; then
echo "Release $TAG published successfully"
if [[ "$GR_OUTCOME" == "failure" ]]; then
echo "::warning::GoReleaser had non-fatal errors" \
"(e.g. homebrew tap update). Release artifacts are fine."
fi
else
echo "::error::Release $TAG was not published"
exit 1
fi
set -euo pipefail
gh release create "$TAG" \
--title "$TAG" \
--notes-file release-notes.md \
--target "$GITHUB_SHA"
13 changes: 0 additions & 13 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,3 @@ jobs:

- name: Run muster integration tests
run: ./muster test --parallel 50 --base-port 30000

- name: Install GoReleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with:
distribution: goreleaser
version: "~> v2"
install-only: true

- name: Run Release Dry-Run
if: github.event_name == 'pull_request'
run: make release-dry-run-fast
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 changes: 0 additions & 29 deletions .github/workflows/zz_generated.release-please.yaml

This file was deleted.

56 changes: 0 additions & 56 deletions .goreleaser.ci.yaml

This file was deleted.

Loading
Loading