diff --git a/.github/workflows/create-release-pr.yaml b/.github/workflows/create-release-pr.yaml index b83e5a8..194f29f 100644 --- a/.github/workflows/create-release-pr.yaml +++ b/.github/workflows/create-release-pr.yaml @@ -27,94 +27,103 @@ jobs: gather_facts: name: Gather facts runs-on: ubuntu-24.04 + permissions: + contents: read outputs: - repo_name: ${{ steps.gather_facts.outputs.repo_name }} - branch: ${{ steps.gather_facts.outputs.branch }} - base: ${{ steps.gather_facts.outputs.base }} - needs_major_bump: ${{ steps.gather_facts.outputs.needs_major_bump }} + repo_name: ${{ steps.parse.outputs.repo_name }} + branch: ${{ steps.parse.outputs.branch }} + base: ${{ steps.parse.outputs.base }} + needs_major_bump: ${{ steps.resolve.outputs.needs_major_bump }} skip: ${{ steps.check_skip.outputs.skip }} - version: ${{ steps.gather_facts.outputs.version }} + version: ${{ steps.resolve.outputs.version }} + is_rc: ${{ steps.resolve.outputs.is_rc }} steps: - - name: Gather facts - id: gather_facts + - name: Parse trigger + id: parse + env: + INPUT_BRANCH: ${{ inputs.branch }} + EVENT_REF: ${{ github.event.ref }} + EVENT_BASE_REF: ${{ github.event.base_ref }} + REPOSITORY: ${{ github.repository }} run: | - head="${{ inputs.branch || github.event.ref }}" + set -euo pipefail + head="${INPUT_BRANCH:-$EVENT_REF}" echo "branch=${head}" >> $GITHUB_OUTPUT - head="${head#refs/heads/}" # Strip "refs/heads/" prefix. - if [[ $(echo "$head" | grep -o '#' | wc -l) -gt 1 ]]; then - base="$(echo $head | cut -d '#' -f 1)" + head_short="${head#refs/heads/}" + if [[ $(echo "$head_short" | grep -o '#' | wc -l) -gt 1 ]]; then + base="$(echo "$head_short" | cut -d '#' -f 1)" else - base="${{ github.event.base_ref }}" + base="${EVENT_BASE_REF}" fi + base="${base#refs/heads/}" - base="${base#refs/heads/}" # Strip "refs/heads/" prefix. + token="$(echo "$head_short" | awk -F# '{print $NF}')" + repo_name="$(echo "$REPOSITORY" | awk -F '/' '{print $2}')" - version="$(echo $head | awk -F# '{print $NF}')" - if [[ $version =~ ^major|minor|patch$ ]]; then - gh auth login --with-token <<<$(echo -n ${{ secrets.TAYLORBOT_GITHUB_ACTION }}) - gh_api_get_latest_release_version() - { - if ! version="$(gh api "repos/$1/releases/latest" --jq '.tag_name[1:] | split(".") | .[0], .[1], .[2]')" - then - case "$version" in - *Not\ Found*) echo Assuming v0.0.0, hooray first release! >&2 ; version="0 0 0" ;; - *) version="" ; return 1 ;; - esac - fi - echo "$version" - } + echo "base=${base}" >> $GITHUB_OUTPUT + echo "token=${token}" >> $GITHUB_OUTPUT + echo "repo_name=${repo_name}" >> $GITHUB_OUTPUT + echo "branch=${head} base=${base} token=${token} repo=${repo_name}" + + - name: Checkout base + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.parse.outputs.base }} + fetch-depth: 0 + persist-credentials: false + + - name: Install gitsemver + uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 + with: + binary: "gitsemver" + # renovate: datasource=github-releases depName=giantswarm/gitsemver + version: "2.0.0" + download_url: "https://github.com/giantswarm/${binary}/releases/download/v${version}/${binary}-v${version}-linux-amd64.tar.gz" + tarball_binary_path: "*/${binary}" + smoke_test: "${binary} --version" - version_parts=($(gh_api_get_latest_release_version "${{ github.repository }}")) - version_major=${version_parts[0]} - version_minor=${version_parts[1]} - version_patch=${version_parts[2]} - case ${version} in - patch) - version_patch=$((version_patch+1)) - ;; - minor) - version_minor=$((version_minor+1)) - version_patch=0 - ;; - major) - version_major=$((version_major+1)) - version_minor=0 - version_patch=0 - if [[ "${version_major}" != "1" ]]; then - echo "needs_major_bump=true" >> $GITHUB_OUTPUT - fi - ;; - *) - echo "Unknown Semver level provided" - exit 1 - ;; - esac - version="${version_major}.${version_minor}.${version_patch}" + - name: Resolve version + id: resolve + env: + TOKEN: ${{ steps.parse.outputs.token }} + run: | + set -euo pipefail + bump_tokens='^(major|minor|patch|major-rc|minor-rc|patch-rc|rc|rc-release)$' + if [[ "$TOKEN" =~ $bump_tokens ]]; then + version="$(gitsemver next "$TOKEN")" else - version="${version#v}" # Strip "v" prefix. - version_major=$(echo "${version}" | cut -d "." -f 1) - version_minor=$(echo "${version}" | cut -d "." -f 2) - version_patch=$(echo "${version}" | cut -d "." -f 3) - # This will help us detect versions with suffixes as majors, i.e 3.0.0-alpha1. - # Even though it's a pre-release, it's still a major. - if [[ $version_minor = 0 && $version_patch =~ ^0.* && $version_major != 1 ]]; then - echo "needs_major_bump=true" >> $GITHUB_OUTPUT + candidate="${TOKEN#v}" + if ! gitsemver validate --type any "$candidate" >/dev/null 2>&1; then + echo "::error::Invalid version '$candidate' (must be valid semver per gitsemver)" + exit 1 fi + version="$candidate" fi - repo_name="$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" - echo "repo_name=\"$repo_name\" base=\"$base\" head=\"$head\" version=\"$version\"" - echo "repo_name=${repo_name}" >> $GITHUB_OUTPUT - echo "base=${base}" >> $GITHUB_OUTPUT - echo "head=${head}" >> $GITHUB_OUTPUT + echo "version=${version}" echo "version=${version}" >> $GITHUB_OUTPUT + # Detect a "new major" release (including its RC) for the go.mod + # upgrade step downstream. A "new major" means X.0.0 (or X.0.0-rc.N) + # with X > 1; X=1 is excluded to preserve legacy behaviour. + core="${version%%-*}" + IFS=. read -r vmaj vmin vpat <<<"$core" + if [[ "$vmin" = "0" && "$vpat" = "0" && "$vmaj" != "1" && "$vmaj" -gt 0 ]]; then + echo "needs_major_bump=true" >> $GITHUB_OUTPUT + fi + + is_rc=false + if gitsemver validate --type rc "$version" >/dev/null 2>&1; then + is_rc=true + fi + echo "is_rc=${is_rc}" >> $GITHUB_OUTPUT + - name: Check if workflow should be skipped id: check_skip env: GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" run: | - head="${{ steps.gather_facts.outputs.branch }}" + head="${{ steps.parse.outputs.branch }}" branch="${head#refs/heads/}" # Strip "refs/heads/" prefix. # Check if PR already exists @@ -124,12 +133,19 @@ jobs: exit 0 fi - # Check if the triggering commit was created by this workflow (has our trailer) - commit_message=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}" --jq '.commit.message' 2>/dev/null || echo "") - if [[ "$commit_message" == *"Release-Workflow-Run:"* ]]; then - echo "Triggering commit was created by release workflow, skipping" - echo "skip=true" >> $GITHUB_OUTPUT - exit 0 + # Check if the triggering commit was created by this workflow (has our trailer). + # Only apply this check when the release branch is ahead of the base — if + # ahead_by is 0, the branch was just created from the base and the trailer + # (if present) belongs to a previous unrelated release on the base branch. + base="${{ steps.parse.outputs.base }}" + ahead_by=$(gh api "repos/${{ github.repository }}/compare/${base}...${branch}" --jq '.ahead_by' 2>/dev/null || echo "0") + if [[ "$ahead_by" -gt "0" ]]; then + commit_message=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}" --jq '.commit.message' 2>/dev/null || echo "") + if [[ "$commit_message" == *"Release-Workflow-Run:"* ]]; then + echo "Triggering commit was created by release workflow, skipping" + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi fi echo "skip=false" >> $GITHUB_OUTPUT diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml index b4ee0ed..5459c3d 100644 --- a/.github/workflows/create-release.yaml +++ b/.github/workflows/create-release.yaml @@ -34,33 +34,55 @@ jobs: contents: read outputs: project_go_path: ${{ steps.get_project_go_path.outputs.path }} - ref_version: ${{ steps.ref_version.outputs.refversion }} version: ${{ steps.get_version.outputs.version }} + is_rc: ${{ steps.get_version.outputs.is_rc }} steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Install gitsemver + uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 + with: + binary: "gitsemver" + # renovate: datasource=github-releases depName=giantswarm/gitsemver + version: "2.0.0" + download_url: "https://github.com/giantswarm/${binary}/releases/download/v${version}/${binary}-v${version}-linux-amd64.tar.gz" + tarball_binary_path: "*/${binary}" + smoke_test: "${binary} --version" - name: Get version id: get_version env: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | - title=$(echo -n "${COMMIT_MESSAGE}" | head -1) - # Matches strings like: - # + set -euo pipefail + # Accept release-PR titles of the form: # - "chore(release): v1.2.3" - # - "chore(release): v1.2.3-r4" + # - "chore(release): v1.2.3-rc.4" # - "chore(release): v1.2.3 (#56)" - # - "chore(release): v1.2.3-r4 (#56)" - # - # The legacy "Release v..." form is also accepted for - # backward compatibility with PRs created by older versions - # of the create-release-pr workflow. - # - # And outputs version part (1.2.3). - if echo "${title}" | grep -iqE '^(chore\(release\):|Release) v[0-9]+\.[0-9]+\.[0-9]+([.-][^ .-][^ ]*)?( \(#[0-9]+\))?$' ; then - version=$(echo "${title}" | cut -d ' ' -f 2) + # The legacy "Release v..." form is also accepted for backward + # compatibility with PRs created by older versions of + # create-release-pr.yaml. Anything else (no match) yields an empty + # version output, which short-circuits the workflow as before. + title=$(echo -n "${COMMIT_MESSAGE}" | head -1) + version="" + is_rc=false + if echo "${title}" | grep -iqE '^(chore\(release\):|Release) v[^ ]+( \(#[0-9]+\))?$' ; then + candidate=$(echo "${title}" | cut -d ' ' -f 2) + candidate="${candidate#v}" + if gitsemver validate --type any "${candidate}" >/dev/null 2>&1; then + version="${candidate}" + if gitsemver validate --type rc "${candidate}" >/dev/null 2>&1; then + is_rc=true + fi + else + echo "::error::Title looks like a release commit but '${candidate}' is not a valid semver per gitsemver." + exit 1 + fi fi - version="${version#v}" # Strip "v" prefix. - echo "version=\"${version}\"" + echo "version=\"${version}\" is_rc=\"${is_rc}\"" echo "version=${version}" >> $GITHUB_OUTPUT + echo "is_rc=${is_rc}" >> $GITHUB_OUTPUT - name: Checkout code if: ${{ steps.get_version.outputs.version != '' }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -74,60 +96,49 @@ jobs: fi echo "path=\"$path\"" echo "path=${path}" >> $GITHUB_OUTPUT - - name: Check if reference version - id: ref_version - env: - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - run: | - title=$(echo -n "${COMMIT_MESSAGE}" | head -1) - if echo "${title}" | grep -iqE '^(chore\(release\):|Release) v[0-9]+\.[0-9]+\.[0-9]+([.-][^ .-][^ ]*)?( \(#[0-9]+\))?$' ; then - version=$(echo "${title}" | cut -d ' ' -f 2) - fi - version=$(echo "${title}" | cut -d ' ' -f 2) - version="${version#v}" # Strip "v" prefix. - refversion=false - if [[ "${version}" =~ ^[0-9]+.[0-9]+.[0-9]+-[0-9]+$ ]]; then - refversion=true - fi - echo "refversion =\"${refversion}\"" - echo "refversion=${refversion}" >> $GITHUB_OUTPUT update_project_go: name: Update project.go runs-on: ubuntu-24.04 permissions: contents: read if: - ${{ needs.gather_facts.outputs.version != '' && needs.gather_facts.outputs.project_go_path != '' && - needs.gather_facts.outputs.ref_version != 'true' }} + ${{ needs.gather_facts.outputs.version != '' && needs.gather_facts.outputs.project_go_path != '' }} needs: - gather_facts steps: - - name: Install architect - uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 - with: - binary: "architect" - version: "6.14.1" - - name: Install semver - uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 - with: - binary: "semver" - version: "3.2.0" - download_url: "https://github.com/fsaintjacques/${binary}-tool/archive/${version}.tar.gz" - tarball_binary_path: "*/src/${binary}" - smoke_test: "${binary} --version" - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + fetch-depth: 0 persist-credentials: false + - name: Install gitsemver + uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 + with: + binary: "gitsemver" + # renovate: datasource=github-releases depName=giantswarm/gitsemver + version: "2.0.0" + download_url: "https://github.com/giantswarm/${binary}/releases/download/v${version}/${binary}-v${version}-linux-amd64.tar.gz" + tarball_binary_path: "*/${binary}" + smoke_test: "${binary} --version" - name: Update project.go id: update_project_go env: branch: "${{ github.ref }}-version-bump" + IS_RC: "${{ needs.gather_facts.outputs.is_rc }}" run: | + set -euo pipefail git checkout -b ${{ env.branch }} file="${{ needs.gather_facts.outputs.project_go_path }}" version="${{ needs.gather_facts.outputs.version }}" - new_version="$(semver bump patch $version)-dev" + if [[ "${IS_RC}" == "true" ]]; then + # An RC like 1.3.0-rc.1 returns project.go to the dev string for the + # stable it is leading toward (1.3.0-dev). This way successive RCs + # do not drift project.go forward and the final stable release lands + # on the version project.go already advertises. + new_version="${version%-rc.*}-dev" + else + new_version="$(gitsemver next patch --last-tag "v${version}")-dev" + fi echo "version=\"$version\" new_version=\"$new_version\"" echo "new_version=${new_version}" >> $GITHUB_OUTPUT sed -Ei "s/(version[[:space:]]*=[[:space:]]*)\"${version}\"/\1\"${new_version}\"/" $file @@ -189,9 +200,7 @@ jobs: ref: ${{ github.sha }} persist-credentials: false - name: Ensure correct version in project.go - if: - ${{ needs.gather_facts.outputs.project_go_path != '' && needs.gather_facts.outputs.ref_version != - 'true' }} + if: ${{ needs.gather_facts.outputs.project_go_path != '' }} run: | file="${{ needs.gather_facts.outputs.project_go_path }}" version="${{ needs.gather_facts.outputs.version }}" @@ -251,6 +260,9 @@ jobs: body: ${{ steps.changelog_reader.outputs.changes }} tag: "v${{ needs.gather_facts.outputs.version }}" token: ${{ secrets.TAYLORBOT_GITHUB_ACTION }} + # Mark release-candidate versions (vX.Y.Z-rc.N) as GitHub pre-releases + # so they don't surface as the repo's "Latest release". + prerelease: ${{ needs.gather_facts.outputs.is_rc == 'true' }} skipIfReleaseExists: true create-release-branch: @@ -260,24 +272,22 @@ jobs: contents: write needs: - gather_facts - if: ${{ needs.gather_facts.outputs.version }} + if: ${{ needs.gather_facts.outputs.version && needs.gather_facts.outputs.is_rc != 'true' }} steps: - - name: Install semver - uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 - with: - binary: "semver" - version: "3.0.0" - download_url: "https://github.com/fsaintjacques/${binary}-tool/archive/${version}.tar.gz" - tarball_binary_path: "*/src/${binary}" - smoke_test: "${binary} --version" - name: Check out the repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 # Clone the whole history, not just the most recent commit. + persist-credentials: false - name: Fetch all tags and branches run: "git fetch --all" - name: Create long-lived release branch + env: + REMOTE_REPO: + "https://${{ github.actor }}:${{ secrets.TAYLORBOT_GITHUB_ACTION }}@github.com/${{ + github.repository }}.git" run: | + set -euo pipefail current_version="${{ needs.gather_facts.outputs.version }}" parent_version="$(git describe --tags --abbrev=0 HEAD^ || true)" parent_version="${parent_version#v}" # Strip "v" prefix. @@ -289,10 +299,11 @@ jobs: echo "current_version=$current_version parent_version=$parent_version" - current_major=$(semver get major $current_version) - current_minor=$(semver get minor $current_version) - parent_major=$(semver get major $parent_version) - parent_minor=$(semver get minor $parent_version) + # Strip any pre-release suffix before splitting on '.', so a parent tag + # that happens to be an RC (e.g. 1.2.3-rc.4) is treated as 1.2.3 for + # the comparison. + IFS=. read -r current_major current_minor _ <<<"${current_version%%-*}" + IFS=. read -r parent_major parent_minor _ <<<"${parent_version%%-*}" echo "current_major=$current_major current_minor=$current_minor" echo "parent_major=$parent_major parent_minor=$parent_minor" @@ -308,13 +319,13 @@ jobs: release_branch="release-v${parent_major}.${parent_minor}.x" echo "release_branch=$release_branch" - if git rev-parse --verify $release_branch ; then + if git rev-parse --verify "$release_branch" >/dev/null 2>&1; then echo "Release branch $release_branch already exists. Nothing to do here." exit 0 fi - git branch $release_branch HEAD^ - git push origin $release_branch + git branch "$release_branch" HEAD^ + git push "${REMOTE_REPO}" "$release_branch" create_and_upload_build_artifacts: name: Create and upload build artifacts @@ -341,6 +352,11 @@ jobs: - create_release - gather_facts steps: + # Transitional: only needed by consumer Makefiles still on the pre-gitsemver + # devctl template, where `VERSION := $(shell architect project version)` + # stamps the release artifacts. devctl's current template uses + # `gitsemver version` (installed below), so this install can be dropped + # once all consumers have regenerated their Makefile.gen.go.mk. - name: Install architect uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 with: @@ -350,6 +366,7 @@ jobs: uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0 with: binary: "gitsemver" + # renovate: datasource=github-releases depName=giantswarm/gitsemver version: "2.0.0" download_url: "https://github.com/giantswarm/${binary}/releases/download/v${version}/${binary}-v${version}-linux-amd64.tar.gz" tarball_binary_path: "*/${binary}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4a31ead..7e1e717 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -153,12 +153,21 @@ jobs: current=$(manifest_version "$base_branch") next=$(manifest_version "$head_branch") - IFS=. read -r cmaj cmin cpat <<<"$current" - IFS=. read -r nmaj nmin npat <<<"$next" + # release-please supports a "prerelease" versioning strategy that + # produces tags like 1.3.0-rc.1. Strip any pre-release suffix before + # the numeric compare so an RC-only step (1.3.0-rc.1 -> 1.3.0-rc.2) + # or the RC -> stable promotion (1.3.0-rc.2 -> 1.3.0) doesn't + # classify as bump=none and silently disable auto-merge. An RC-only + # change of the same X.Y.Z triple is treated as a patch-level bump. + current_core="${current%%-*}" + next_core="${next%%-*}" + IFS=. read -r cmaj cmin cpat <<<"$current_core" + IFS=. read -r nmaj nmin npat <<<"$next_core" if [ "$nmaj" -gt "$cmaj" ]; then bump=major elif [ "$nmin" -gt "$cmin" ]; then bump=minor elif [ "$npat" -gt "$cpat" ]; then bump=patch + elif [ "$current" != "$next" ]; then bump=patch else bump=none fi diff --git a/.github/workflows/validate-changelog.yaml b/.github/workflows/validate-changelog.yaml index 84a2f96..8920c20 100644 --- a/.github/workflows/validate-changelog.yaml +++ b/.github/workflows/validate-changelog.yaml @@ -31,12 +31,22 @@ jobs: version="" - # Use case statement for more reliable pattern matching - case "$branch_name" in - *"#release#v"*) - # Extract version from patterns like main#release#v1.2.3 - version=$(echo "$branch_name" | sed -E 's/.*#release#v([0-9]+\.[0-9]+\.[0-9]+).*/\1/') - if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + # The release token is the segment after the last '#', mirroring how + # create-release-pr.yaml parses it (awk -F# '{print $NF}'). This makes + # matching independent of the base-branch prefix, so both naming schemes + # work: "main#release#minor-rc" and the prefix-less "release#minor-rc". + token="${branch_name##*#}" + echo "Release token: $token" + + version="" + + # A semver core (X.Y.Z) may carry an optional release-candidate suffix + # (-rc.N) that gitsemver produces, so the version patterns allow it. + case "$token" in + v[0-9]*) + # Explicit version token like v1.2.3 or v1.2.3-rc.4 + version="${token#v}" + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then echo "Extracted explicit version: $version" else echo "::error::Failed to extract version from: $branch_name" @@ -44,18 +54,20 @@ jobs: fi ;; - *"#release#major"|*"#release#minor"|*"#release#patch") - # Extract bump type and get version from changelog - bump_type=$(echo "$branch_name" | sed -E 's/.*#release#(major|minor|patch).*/\1/') - echo "🔍 Detected semantic version branch ($bump_type), extracting version from changelog..." + major|minor|patch|major-rc|minor-rc|patch-rc|rc|rc-release) + # Bump-token branch: the concrete version was resolved by gitsemver + # and written into the changelog by architect, so read it from there. + # This covers both stable bumps and their RC variants uniformly. + echo "🔍 Detected bump-token release branch ($token), extracting version from changelog..." if [ ! -f "CHANGELOG.md" ]; then echo "::error::CHANGELOG.md not found" exit 1 fi - # Get the first version entry (should be the newest one that architect created) - version=$(grep -E "^## \[[0-9]+\.[0-9]+\.[0-9]+\]" CHANGELOG.md | head -1 | sed -E 's/^## \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/') + # Get the first version entry (should be the newest one that architect + # created); allow an optional -rc.N suffix for release candidates. + version=$(grep -E "^## \[[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\]" CHANGELOG.md | head -1 | sed -E 's/^## \[([0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?)\].*/\1/') if [ -z "$version" ]; then echo "::error::Could not find version in CHANGELOG.md" @@ -65,20 +77,16 @@ jobs: exit 1 fi - echo "Extracted version from changelog for $bump_type release: $version" + echo "Extracted version from changelog for $token release: $version" ;; *) - echo "::error::Could not extract version from branch name: $branch_name" - echo "::error::Expected patterns:" - echo " - main#release#v1.2.3" - echo " - main#release#major" - echo " - main#release#minor" - echo " - main#release#patch" - echo " - master#release#v1.2.3" - echo " - master#release#major|minor|patch" - echo " - release#v1.2.3" - echo " - release#major|minor|patch" + echo "::error::Could not extract version from branch name: $branch_name (token: $token)" + echo "::error::Expected the branch to end in one of:" + echo " - #v1.2.3 (optionally -rc.N, e.g. #v1.2.3-rc.4)" + echo " - #major | #minor | #patch" + echo " - #major-rc | #minor-rc | #patch-rc" + echo " - #rc | #rc-release" exit 1 ;; esac diff --git a/CHANGELOG.md b/CHANGELOG.md index b18b56c..38105d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), however this project does not use Semantic Versioning and there are no releases. Instead this file uses a date-based structure. +## 2026-06-03 + +### Fixed + +- `create-release.yaml` now marks release-candidate releases (`vX.Y.Z-rc.N`) as GitHub pre-releases by passing `prerelease` to `ncipollo/release-action`, derived from the existing `gather_facts.is_rc` output. Previously RC tags were published as full releases and could become the repo's "Latest release". +- `validate-changelog.yaml` now recognises release-candidate branches. It parses the release token from the segment after the last `#` (mirroring `create-release-pr.yaml`) instead of pattern-matching the whole branch, so it accepts the RC bump tokens (`major-rc`, `minor-rc`, `patch-rc`, `rc`, `rc-release`) and explicit RC versions (`#v1.2.3-rc.4`) in addition to the stable tokens. The changelog version regex now allows an optional `-rc.N` suffix. As a side effect this also fixes the prefix-less `release#` naming scheme, which the previous `*#release#` patterns never matched. +- `create-release-pr.yaml`'s `check_skip` step no longer false-positively skips when an RC (or any) release branch is pushed from a base-branch HEAD that already carries a `Release-Workflow-Run:` trailer from a previous release. The trailer check now only applies when the release branch is actually ahead of the base (i.e., the workflow already committed to it), so freshly-pushed RC branches like `main#release#minor-rc` correctly proceed to create the release PR. +- `create-release.yaml`'s `create-release-branch` job now pushes the new long-lived maintenance branch using the `TAYLORBOT_GITHUB_ACTION` token (same authenticated remote URL pattern used by every other push in the file), rather than a bare `git push origin` that silently fails when `persist-credentials: false` is set. This bug was pre-existing on `main` and would have silently skipped release-branch creation on every minor/major release. + +## 2026-06-01 + +### Changed + +- `create-release-pr.yaml` and `create-release.yaml` install [`giantswarm/gitsemver`](https://github.com/giantswarm/gitsemver) `v1.1.2` via `giantswarm/install-binary-action` (the same mechanism already used for `architect` and Renovate-tracked), replacing the short-lived local composite action `.github/actions/gitsemver-install`. A local composite referenced as `uses: ./.github/actions/...` does not resolve inside a reusable (`workflow_call`) workflow — the runner looks the path up in the **caller** repository's checkout, not in `github-workflows` — so it failed for every consumer of these workflows. `v1.1.2` also fixes the `gitsemver --version` self-info flag, which the install step's smoke test now uses. This aligns these workflows with `architect-orb` v9.0.0, where `gitsemver` is the single source of git-based semver. +- `create-release.yaml`'s build-artifacts job keeps installing `architect` purely as a transition aid: consumer Makefiles still on the pre-gitsemver `devctl` template stamp release artifacts with `VERSION := $(shell architect project version)`. `devctl`'s current `Makefile.gen.go.mk` template uses `gitsemver version` (already installed in that job), so the `architect` install becomes removable once all consumers regenerate their Makefile. No version or git tag in these workflows is produced by `architect` — `gitsemver` is the sole source (`architect prepare-release` only consumes the already-resolved `--version`). + +### Removed + +- `create-release.yaml` no longer installs the `architect` binary in the `update_project_go` job. It was installed but never invoked — the post-release `-dev` bump of `project.go` is computed entirely with `gitsemver next patch` plus a `sed` rewrite. +- `create-release.yaml` drops the leftover `needs.gather_facts.outputs.ref_version != 'true'` guards on the `update_project_go` job and the `Ensure correct version in project.go` step. The `ref_version` output was removed together with the legacy reference-version handling, so the guards always evaluated truthy and only obscured the real conditions. + +## 2026-05-28 + +### Added + +- `create-release-pr.yaml` accepts new bump tokens on the trigger branch (`branch#`): `patch-rc`, `minor-rc`, `major-rc`, `rc`, `rc-release`. They are passed verbatim to `gitsemver next` and produce release-candidate versions like `v1.3.0-rc.1`, `v1.3.0-rc.2`, then `v1.3.0` via `rc-release`. The existing `patch` / `minor` / `major` tokens continue to work unchanged. +- `create-release.yaml` exposes a new `is_rc` output on its `gather_facts` job (true when the released tag is an RC per `gitsemver validate --type rc`). + +### Changed + +- `create-release-pr.yaml` now computes the next version with `gitsemver next ` instead of the inline `gh api releases/latest` + manual increment. Explicit-version trigger branches (`branch#vX.Y.Z[-rc.N]`) are now validated by `gitsemver validate --type any` and rejected on invalid input — strings that previously slipped through the loose regex (e.g. `1.2.3.foo`) are now hard failures with a clear error. The job now checks out the base branch with `fetch-depth: 0` and `persist-credentials: false` so gitsemver can see full tag history. +- `create-release.yaml` validates the version parsed from the release-PR commit title with `gitsemver` rather than an inline regex. RC tags (`vX.Y.Z-rc.N`) are first-class: they create the tag and GH release like a stable version, and they DO still trigger the post-release `-dev` bump of `project.go` (the new dev string targets the stable the RC is leading toward, e.g. `1.3.0-rc.1` ➝ `1.3.0-dev`). The long-lived `release-vX.Y.x` branch is only cut for stable major/minor releases — RC releases skip that job. +- `release.yaml` (release-please) auto-merge reconciler now strips any pre-release suffix from the manifest versions before the numeric `X.Y.Z` compare, so a `1.3.0-rc.1 → 1.3.0-rc.2` PR (or an RC ➝ stable promotion) is classified as `bump=patch` rather than silently `bump=none`. Auto-merge therefore honours the configured `auto-merge-level` ceiling on RC PRs too. Consumers wanting RC support on this path enable it in their own `release-please-config.json` with `"versioning": "prerelease"`, `"prerelease": true`, `"prerelease-type": "rc"` — `release.yaml` itself stays single-code-path. + +### Removed + +- `create-release.yaml` no longer recognises the legacy "reference version" form `vX.Y.Z-N` (e.g. `v1.2.3-4`) — the dedicated `ref_version` job and its special-case regex are gone. Any repo still pushing such tags via this workflow will need to migrate to the RC form (`vX.Y.Z-rc.N`). +- `create-release.yaml` and `create-release-pr.yaml` no longer install `fsaintjacques/semver-tool`; all next-version arithmetic now goes through `gitsemver` (or bash parameter expansion on a version string already validated by it). ## 2026-06-18 ### Fixed