From 3fea2b9ac0d46ea16ca5bd4809608024a0edf786 Mon Sep 17 00:00:00 2001 From: Jose Armesto Date: Wed, 3 Jun 2026 16:12:54 +0200 Subject: [PATCH 1/4] ci: migrate to push-based release flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces all the existing release-related workflows / configs with the push-based auto-release flow validated end-to-end in giantswarm/test-release-please. Removed: release-please workflow/config/manifest, .goreleaser.yaml, .goreleaser.ci.yaml, and the colleague-PoC auto-release.yaml. Added: - .github/workflows/auto-release.yaml (single workflow: git-cliff decides bump, creates tag + GitHub Release atomically via 'gh release create') - cliff.toml (Keep-a-Changelog sections, PR-link + author rendering via [remote.github] integration, '(?m) (#N)$' preprocessor for squash-merge suffixes, Full Changelog footer, --unreleased --bump flag combo to filter to the bumped release only) CHANGELOG.md left alone — historical reference; future releases publish notes only to the GitHub Releases page. CircleCI unchanged: it already had architect/upload-release-assets for the muster binary. --- .github/workflows/auto-release.yaml | 246 +++++++++--------- .../zz_generated.release-please.yaml | 29 --- .goreleaser.ci.yaml | 56 ---- .goreleaser.yaml | 156 ----------- .release-please-manifest.json | 3 - cliff.toml | 107 ++++++++ release-please-config.json | 21 -- 7 files changed, 229 insertions(+), 389 deletions(-) delete mode 100644 .github/workflows/zz_generated.release-please.yaml delete mode 100644 .goreleaser.ci.yaml delete mode 100644 .goreleaser.yaml delete mode 100644 .release-please-manifest.json create mode 100644 cliff.toml delete mode 100644 release-please-config.json diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml index 907f71199..e41753fe9 100644 --- a/.github/workflows/auto-release.yaml +++ b/.github/workflows/auto-release.yaml @@ -1,146 +1,144 @@ --- # 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 # needed to push tags and create the GitHub Release + pull-requests: read # needed for git-cliff's GitHub API lookups + # (commit.github.pr_number / username in release notes) + +# 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" + 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' cliff-context.json) + echo "next computed: ${NEXT:-}" + 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:-}" + 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" diff --git a/.github/workflows/zz_generated.release-please.yaml b/.github/workflows/zz_generated.release-please.yaml deleted file mode 100644 index e6b9701f3..000000000 --- a/.github/workflows/zz_generated.release-please.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# DO NOT EDIT. Generated with: -# -# devctl -# -# https://github.com/giantswarm/devctl/blob/4129e812666c6bf5584d68d971dc6ce6c6eb5cc9/pkg/gen/input/workflows/internal/file/release_please.yaml.template -# -name: Release Please - -on: - push: - branches: - - main - - master - -permissions: {} - -jobs: - release_please: - uses: giantswarm/github-workflows/.github/workflows/release.yaml@main - permissions: - contents: write - pull-requests: write - with: - auto-merge-level: minor - secrets: - RELEASE_PLEASE_CLIENT_ID: ${{ secrets.RELEASE_PLEASE_CLIENT_ID }} - RELEASE_PLEASE_PRIVATE_KEY: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }} - RELEASE_PLEASE_APPROVER_CLIENT_ID: ${{ secrets.RELEASE_PLEASE_APPROVER_CLIENT_ID }} - RELEASE_PLEASE_APPROVER_PRIVATE_KEY: ${{ secrets.RELEASE_PLEASE_APPROVER_PRIVATE_KEY }} diff --git a/.goreleaser.ci.yaml b/.goreleaser.ci.yaml deleted file mode 100644 index 4b3c09b25..000000000 --- a/.goreleaser.ci.yaml +++ /dev/null @@ -1,56 +0,0 @@ ---- -# yamllint disable rule:truthy -# .goreleaser.ci.yaml -# Minimal GoReleaser config for CI dry-run validation. -# Only builds linux/amd64 to validate the release config works. -# The full release uses .goreleaser.yaml which builds all platforms. -version: 2 - -before: - hooks: - - go mod tidy - -builds: - - id: muster - main: ./main.go - binary: muster - # Only build linux/amd64 for CI validation - saves ~6 minutes - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=0 - mod_timestamp: '{{ .CommitTimestamp }}' - flags: - - -trimpath - ldflags: - - >- - -s -w - -X github.com/giantswarm/muster/pkg/project.version={{.Version}} - -X github.com/giantswarm/muster/pkg/project.gitSHA={{.ShortCommit}} - -X github.com/giantswarm/muster/pkg/project.buildTimestamp={{.Date}} - -archives: - - id: default - formats: [tar.gz] - name_template: >- - {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - wrap_in_directory: true - files: - - LICENSE - - README.md - -checksum: - algorithm: sha256 - name_template: '{{ .ProjectName }}_checksums.txt' - -# Skip changelog generation in CI - not needed for validation -changelog: - disable: true - -# Modelines -# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index 3979d7283..000000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,156 +0,0 @@ ---- -# yamllint disable rule:truthy -# .goreleaser.yaml -# Make sure to check the documentation at https://goreleaser.com -version: 2 # Explicitly set GoReleaser configuration version - -before: - hooks: - # You may remove this if you don't use go modules. - - go mod tidy - # you may remove this if you don't need go generate - # - go generate ./... - -builds: - - id: muster - # Path to main.go file or main package. - main: ./main.go - # Binary name. - # Defaults to the project name. - binary: muster - # GOOS list to build for. - # Defaults to linux, windows and darwin. - goos: - - linux - - windows - - darwin - # GOARCH list to build for. - # Defaults to 386, amd64 and arm64. - goarch: - - amd64 - - arm64 - # GOARM list to build for when GOARCH is arm. - # Defaults to 6. - # goarm: - # - "6" - # Environment variables that will be passed to the build process. - env: - - CGO_ENABLED=0 - # Set the modified timestamp on the output binary, typically - # you would do this to ensure reproducible builds. - mod_timestamp: '{{ .CommitTimestamp }}' - # Flags to pass to the go build command. - # Defaults to -trimpath. - flags: - - -trimpath - # Ldflags inject build metadata into pkg/project. Same vars are populated - # by architect-orb's go-build job for container images. - ldflags: - - >- - -s -w - -X github.com/giantswarm/muster/pkg/project.version={{.Version}} - -X github.com/giantswarm/muster/pkg/project.gitSHA={{.ShortCommit}} - -X github.com/giantswarm/muster/pkg/project.buildTimestamp={{.Date}} - -archives: - - id: default - # Default formats for all platforms (can be overridden per OS) - formats: [tar.gz] - name_template: >- - {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }} - # Wrap the binary in a directory structure when extracted. - # Defaults to false. - wrap_in_directory: true - # Format overrides per OS (using new plural 'formats' syntax) - format_overrides: - - goos: windows - formats: [zip] - - goos: darwin - formats: [zip] - # Files to include in the archive. - # Defaults to license*, readme*, etc. - files: - - LICENSE - - README.md - -checksum: - # Algorithm to use. Valid options are sha256, sha512, sha1, crc32, md5, etc. - # Default is sha256. - algorithm: sha256 - # Name template for the checksums file. - # Default is `{{ .ProjectName }}_{{ .Version }}_checksums.txt`. - name_template: '{{ .ProjectName }}_checksums.txt' - -changelog: - # Use GitHub's native release notes generation. - # This automatically uses PRs merged since the last tag. - # Disables sort, groups, filters, etc. - use: github-native - -release: - # Repo in format "owner/name". - # Defaults to the origin remote. - # repo: - # owner: user - # name: repo - # If set to true, will not auto-publish the release. - # Default is false. - draft: false - # If set to true, will mark the release as not ready for production. - # Default is false. - prerelease: auto - # You can change the name of the GitHub release. - # Default is `{{.Tag}}` - name_template: "{{.ProjectName}} v{{.Version}}" - # Header to add to the release notes. - # Default is empty. - # header: | - # This is the header - # Footer to add to the release notes. - # Default is empty. - # footer: | - # This is the footer - # Add back the extra_files section with corrected globs - extra_files: - - glob: ./dist/muster_darwin_amd64*/muster - name_template: muster_darwin_amd64 - - glob: ./dist/muster_darwin_arm64*/muster - name_template: muster_darwin_arm64 - - glob: ./dist/muster_linux_amd64*/muster - name_template: muster_linux_amd64 - - glob: ./dist/muster_linux_arm64*/muster - name_template: muster_linux_arm64 - - glob: ./dist/muster_windows_amd64*/muster.exe - name_template: muster_windows_amd64.exe - - glob: ./dist/muster_windows_arm64*/muster.exe - name_template: muster_windows_arm64.exe - -brews: - - name: muster - repository: - owner: giantswarm - name: homebrew-muster - branch: main - token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" - url_template: >- - https://github.com/giantswarm/muster/releases/download/{{ .Tag }}/ - {{- .ArtifactName }} - commit_author: - name: giantswarm-bot - email: bot@giantswarm.io - commit_msg_template: "Update {{ .ProjectName }} to {{ .Tag }}" - directory: Formula - homepage: "https://github.com/giantswarm/muster" - description: "Universal Control Plane for AI Agents - MCP server aggregator" - license: "Apache-2.0" - skip_upload: auto - install: | - bin.install "muster" - test: | - system "#{bin}/muster", "version" - -# Modelines -# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 2be9c43c8..000000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "0.2.0" -} diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 000000000..646681d1f --- /dev/null +++ b/cliff.toml @@ -0,0 +1,107 @@ +# git-cliff config for the push-based PoC. +# +# Two jobs: +# 1. Decide the next semver from conventional commits (`--bumped-version`, +# used by .github/workflows/auto-tag.yaml). +# 2. Render the release notes posted on the GitHub Release (`--latest`, +# also used by auto-tag.yaml after the tag is pushed). + +[bump] +# `feat:` always bumps minor (not patch). Matches conventional-commits spec. +features_always_bump_minor = true +# `feat!:`/`BREAKING CHANGE` always bumps major. +breaking_always_bump_major = true + +[git] +conventional_commits = true +# Drop non-conventional commits silently rather than counting them as +# unclassified work — the bump-decider should only act on intent-bearing +# commits. +filter_unconventional = true +# Highest-version tag (not creation date) is the baseline when computing what +# changed since last release. +topo_order = false +sort_commits = "oldest" + +# Strip the trailing " (#N)" that GitHub appends to squash-merge commit +# messages. Our template renders the PR link explicitly via cliff's GitHub +# integration (commit.remote.pr_number — see [changelog].body), so the +# auto-appended reference would duplicate it. +# +# `(?m)` enables multi-line mode so `$` matches end-of-line (the squash +# `(#N)` lives at the end of the subject line, not the end of the full +# commit text — git-cliff's default regex mode is single-line). This +# anchoring keeps `(#N)` references that appear inside commit bodies +# untouched. +commit_preprocessors = [ + { pattern = '(?m) \(#\d+\)$', replace = "" }, +] + +# Map commit types to changelog groups. Mirrors the section structure from +# the old release-please-config.json (Keep-a-Changelog convention): feat → +# Added, fix → Fixed, all "internal change" types → Changed, security → +# Security. +# +# `style` is the only type marked skip — release-please-config.json didn't +# include it anywhere either. Bump decision is unaffected by these group +# names: git-cliff only bumps on feat / fix / breaking regardless. +commit_parsers = [ + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^refactor", group = "Changed" }, + { message = "^perf", group = "Changed" }, + { message = "^docs", group = "Changed" }, + { message = "^chore", group = "Changed" }, + { message = "^test", group = "Changed" }, + { message = "^build", group = "Changed" }, + { message = "^ci", group = "Changed" }, + { message = "^security", group = "Security" }, + { message = "^style", skip = true }, +] + +# GitHub repo coordinates — required for the API lookups that populate +# `commit.github.pr_number` and `commit.github.username` in the template +# below. The token is passed automatically by git-cliff-action from the +# workflow's GITHUB_TOKEN. +# +# Note for devctl when templating this file: owner / repo need to come from +# the consuming repo's identity, not be hard-coded. +[remote.github] +owner = "giantswarm" +repo = "muster" + +# Template for the rendered release notes. Body is what gets posted to the +# GitHub Release; header and footer are empty so the release page shows only +# the section list. +# +# Each bullet ends with " in [#N](pr-url) by [@user](user-url)" when +# git-cliff successfully resolves the PR for the commit via the GitHub API. +# `commit.remote` is the post-2.x replacement for the per-platform fields +# (commit.github / commit.gitlab / etc.) — using it future-proofs the +# template against git-cliff dropping the old names. +# Backslashes at line ends escape the newline so the bullet stays on one +# line in the rendered Markdown. +[changelog] +header = "" +# `{%-` / `-%}` strip the surrounding newline so the rendered Markdown is +# tight: one blank line before each "### " header, no leading blank, no +# double-blank between header and bullets. Without the whitespace control +# the output picks up two extra blank lines per loop iteration. +body = """ +{% for group, commits in commits | group_by(attribute="group") -%} +### {{ group }} + +{% for commit in commits -%} +- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ +{% if commit.breaking %}[**breaking**] {% endif %}\ +{{ commit.message | split(pat="\n") | first | upper_first }}\ +{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }}){% endif %}\ +{% if commit.remote.username %} by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }}){% endif %} +{% endfor %} +{% endfor -%} +{% if previous.version %} +**Full Changelog**: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/compare/{{ previous.version }}...{{ version }} +{% endif -%} +""" +trim = true +footer = "" diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index d69d6c3a2..000000000 --- a/release-please-config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "release-type": "simple", - "packages": { - ".": {} - }, - "extra-files": [ - {"type": "generic", "path": "pkg/project/project.go"} - ], - "changelog-sections": [ - {"type": "feat", "section": "Added"}, - {"type": "fix", "section": "Fixed"}, - {"type": "refactor", "section": "Changed"}, - {"type": "perf", "section": "Changed"}, - {"type": "docs", "section": "Changed"}, - {"type": "chore", "section": "Changed"}, - {"type": "test", "section": "Changed"}, - {"type": "build", "section": "Changed"}, - {"type": "ci", "section": "Changed"}, - {"type": "security", "section": "Security"} - ] -} From 10f785e32db82ca626b148e8a5ce6ca91258d8bb Mon Sep 17 00:00:00 2001 From: Jose Armesto Date: Wed, 3 Jun 2026 16:36:58 +0200 Subject: [PATCH 2/4] fix: address muster-specific lint failures from the migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues surfaced when this PR hit muster's CI: 1. Makefile.custom.mk referenced the now-deleted .goreleaser.yaml / .goreleaser.ci.yaml in three release-* targets and the lint-yaml target. The release-* targets (release-dry-run, release-dry-run-fast, release-local) are dead with goreleaser gone; remove them entirely. Also drop the test-auto-release target (act-based simulation of the old goreleaser auto-release.yaml shape — doesn't apply to the new workflow). Trim the lint-yaml file list to just the surviving workflow YAMLs. 2. auto-release.yaml line 62 (the git-cliff download_url) was 143 chars, over muster's 120-char yamllint limit (vs. test-release-please's 200). Add `# yamllint disable-line rule:line-length` immediately above it — URLs don't have natural break points and this is the standard escape hatch. Also tidied a small comment-indentation warning in the same file: the trailing comment on `pull-requests: read` had a wrapped second line that yamllint flagged as misaligned. Restructured the comment block so the explanation sits above the permission line instead of trailing it. Verified locally with the same yamllint config muster's CI uses (.yamllint, line-length max 120) — both files lint clean. --- .github/workflows/auto-release.yaml | 8 +++++--- Makefile.custom.mk | 23 +---------------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml index e41753fe9..615f5f62f 100644 --- a/.github/workflows/auto-release.yaml +++ b/.github/workflows/auto-release.yaml @@ -27,9 +27,10 @@ on: - 'release-*' # maintenance branches for backports, e.g. release-2.x permissions: - contents: write # needed to push tags and create the GitHub Release - pull-requests: read # needed for git-cliff's GitHub API lookups - # (commit.github.pr_number / username in release notes) + 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 @@ -59,6 +60,7 @@ jobs: with: 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' diff --git a/Makefile.custom.mk b/Makefile.custom.mk index 34eebf1a5..cf907b4de 100644 --- a/Makefile.custom.mk +++ b/Makefile.custom.mk @@ -1,27 +1,13 @@ # Custom Makefile targets for muster # This file is included by the main Makefile via `include Makefile.*.mk` -##@ Release - -.PHONY: release-dry-run -release-dry-run: ## Test the release process without publishing (all platforms) - goreleaser release --snapshot --clean --skip=announce,publish,validate - -.PHONY: release-dry-run-fast -release-dry-run-fast: ## Fast release dry-run for CI (linux/amd64 only, ~6min faster) - goreleaser release --config .goreleaser.ci.yaml --snapshot --clean --skip=announce,publish,validate - -.PHONY: release-local -release-local: ## Create a release locally - goreleaser release --clean - ##@ Development .PHONY: lint-yaml lint-yaml: ## Run YAML linter @echo "Running YAML linter..." @# Exclude zz_generated files - @yamllint .github/workflows/auto-release.yaml .github/workflows/ci.yaml .goreleaser.yaml .goreleaser.ci.yaml + @yamllint .github/workflows/auto-release.yaml .github/workflows/ci.yaml .PHONY: helm-lint helm-lint: ## Run Helm linter @@ -63,10 +49,3 @@ test-ci-pr: ## Run 'act' to simulate CI checks for a pull request test-ci-push: ## Run 'act' to simulate CI checks for a push to main @echo "Simulating CI workflow (push event)..." @act push --job check - -.PHONY: test-auto-release -test-auto-release: ## Run 'act' to simulate the auto-release workflow - @echo "Simulating Auto-Release workflow (merged pull_request event)..." - @echo "NOTE: Requires 'merged_pr_event.json' in the project root." - @echo "NOTE: Git push steps within the workflow are expected to fail locally." - @act pull_request --job auto_release --eventpath merged_pr_event.json From 5523e92ef0e8796b837f350adb213bb0be2d2a9c Mon Sep 17 00:00:00 2001 From: Jose Armesto Date: Wed, 3 Jun 2026 16:39:40 +0200 Subject: [PATCH 3/4] ci: drop goreleaser dry-run step from ci.yaml + CLAUDE.md mention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ci.yaml still installed goreleaser and ran `make release-dry-run-fast` on PRs as a "release smoke test". With goreleaser gone, both the target and the test no longer apply. Drop the two steps. Also removed the now-stale "Release" section from CLAUDE.md's make-targets list — referenced the deleted release-dry-run-fast target and would mislead anyone reading the docs. --- .github/workflows/ci.yaml | 13 ------------- CLAUDE.md | 3 --- 2 files changed, 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f03c4aab0..13d990100 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 }} diff --git a/CLAUDE.md b/CLAUDE.md index ed0a066ed..c4d9e3138 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,9 +27,6 @@ make vet # go vet # Helm make helm-lint # Lint Helm chart make helm-test # Helm unit tests - -# Release -make release-dry-run-fast # Quick release test (linux/amd64 only) ``` ## Architecture From 1e6249867e12c5e1a75bf5ed51bfcb0b1d192de9 Mon Sep 17 00:00:00 2001 From: Jose Armesto Date: Wed, 3 Jun 2026 16:51:11 +0200 Subject: [PATCH 4/4] fix: jq '// empty' guard against null version slipping through From teemow's review of muster#792. If git-cliff ever returns an empty array (no unreleased commits / no bumpable commits) the current `jq -r '.[0].version'` would output the literal "null", slipping past the [ -z ] / [ = last ] skip guards and attempting `gh release create null`. `// empty` makes jq output nothing instead, which [ -z ] catches. Mirrored from giantswarm/test-release-please#17. --- .github/workflows/auto-release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml index 615f5f62f..ec81a7788 100644 --- a/.github/workflows/auto-release.yaml +++ b/.github/workflows/auto-release.yaml @@ -90,7 +90,7 @@ jobs: # 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' cliff-context.json) + NEXT=$(jq -r '.[0].version // empty' cliff-context.json) echo "next computed: ${NEXT:-}" echo "version=${NEXT}" >> "$GITHUB_OUTPUT" # Cheap render from the cached context — no API calls, no git walk.