From fe5e0572b22c1d28dd702067125ea898c0099640 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Fri, 26 Jun 2026 13:45:50 -0700 Subject: [PATCH 1/8] chore(release): add release workflows --- .github/workflows/prepare-release.yml | 102 ++++++++++++++++++++++++++ .github/workflows/release.yml | 71 ++++++++++++++++++ docs/developer/how-to/releasing.md | 51 +++++++++++-- 3 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..9c679b38 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,102 @@ +name: "Prepare release" + +# Manually triggered. Generates the changelog draft on a release branch and +# pushes it. It does NOT open a PR (that needs auth we avoid) -- a maintainer +# opens the PR from the pushed branch, curates the draft, and merges it. Tagging +# happens afterwards in the "Release" workflow. See +# docs/developer/how-to/releasing.md. +on: + workflow_dispatch: + +# Don't run two release preparations at once. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: {} + +jobs: + prepare-release: + name: "Prepare release branch" + runs-on: ubuntu-latest + permissions: + contents: write # Push the release branch. + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + # git-cliff needs full history and tags to compute the next version. + fetch-depth: 0 + persist-credentials: false + + - name: Get go tools + uses: ./.github/getgotools + + - name: Read pinned git-cliff version + id: gitcliff + run: | + set -euo pipefail + version="$(sed -n 's/^[[:space:]]*git-cliff = "=\([0-9.]*\)".*/\1/p' tools/git-cliff/Cargo.toml)" + if [ -z "$version" ]; then + echo "could not read git-cliff version from tools/git-cliff/Cargo.toml" >&2 + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Install git-cliff + uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2.82.5 + with: + tool: git-cliff@${{ steps.gitcliff.outputs.version }} + + - name: Generate changelog draft + run: mage changelog + + - name: Push release branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + set -euo pipefail + + # Nothing to release: `mage changelog` left CHANGELOG.md unchanged (no new entries since + # the last release). Report it in the job summary and stop -- this is not a failure. + if git diff --quiet -- CHANGELOG.md; then + { + echo "### Nothing to release" + echo "" + echo "No new changelog entries since the last release, so no release branch was created." + } >> "$GITHUB_STEP_SUMMARY" + echo "Nothing to release: CHANGELOG.md is unchanged." + exit 0 + fi + + version="$(sed -n 's/^## \[\([0-9.]*\)\].*/\1/p' CHANGELOG.md | head -1)" + if [ -z "$version" ]; then + echo "no '## [X.Y.Z]' heading found in CHANGELOG.md" >&2 + exit 1 + fi + branch="release/v${version}" + + # Refuse to clobber an existing release branch (e.g. a prior prepare-release run that + # has not been merged or cleaned up yet). + if git ls-remote --exit-code --heads origin "refs/heads/${branch}" >/dev/null 2>&1; then + echo "Release branch '${branch}' already exists on the remote; merge or delete it before preparing again." >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git switch -c "$branch" + git add CHANGELOG.md + git commit -m "chore: prepare release v${version}" + git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$branch" + + # Build the create-PR URL from the current server/repo so it points at this repo (fork or + # upstream). GitHub's pull/new/ form targets the repo's default branch as the base. + pr_url="${SERVER_URL}/${REPO}/pull/new/${branch}" + { + echo "Pushed branch \`$branch\` with the v${version} changelog draft." + echo "" + echo "**[Open the pull request →](${pr_url})**, curate the changelog into user-facing notes, then merge." + } >> "$GITHUB_STEP_SUMMARY" + echo "Open a PR: ${pr_url}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5905eae1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: "Release" + +# Runs on every push to main. When CHANGELOG.md carries a new version, tags it +# (via the idempotent `mage release`), pushes the tag, and publishes a GitHub +# Release with that version's changelog notes. Normal merges are a no-op because +# the changelog's top version is already tagged. See docs/developer/how-to/releasing.md. +on: + push: + branches: [main] + +# Don't run two releases at once. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: {} + +jobs: + release: + name: "Tag release" + runs-on: ubuntu-latest + permissions: + contents: write # Push the release tag. + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + # Need full history and tags to detect existing releases and tag HEAD. + fetch-depth: 0 + persist-credentials: false + + - name: Get go tools + uses: ./.github/getgotools + + - name: Create release tag from changelog + run: | + # Annotated tags (git tag -a) need a committer identity, which the runner lacks. + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + mage release + + - name: Publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + version="$(sed -n 's/^## \[\([0-9.]*\)\].*/\1/p' CHANGELOG.md | head -1)" + if [ -z "$version" ]; then + echo "no '## [X.Y.Z]' heading found in CHANGELOG.md" >&2 + exit 1 + fi + tag="v${version}" + if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then + echo "Tag \`${tag}\` already on remote; nothing to release." >> "$GITHUB_STEP_SUMMARY" + else + git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "refs/tags/${tag}" + # Release notes = this version's changelog section (its body, without the heading line). + awk '/^## \[/{n++; next} n==1' CHANGELOG.md > "${RUNNER_TEMP}/release-notes.md" + gh release create "${tag}" --repo "${REPO}" --title "${tag}" --notes-file "${RUNNER_TEMP}/release-notes.md" + echo "Published release \`${tag}\` with notes from CHANGELOG.md." >> "$GITHUB_STEP_SUMMARY" + + # Warm the module proxy and pkg.go.dev so the new version is discoverable promptly. + # Upstream only: a fork declares the upstream module path in go.mod, so the proxy would + # reject a request under the fork's path -- warming there just errors. Best-effort. + if [ "${REPO}" = "microsoft/azure-linux-dev-tools" ]; then + module="$(awk '/^module /{print $2; exit}' go.mod)" + GOPROXY=https://proxy.golang.org go list -m "${module}@${tag}" \ + || echo "proxy warm deferred; it will index on first use" + curl -fsS -o /dev/null "https://pkg.go.dev/${module}@${tag}" || true + fi + fi diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index 2a753763..fd44b2f8 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -5,6 +5,22 @@ This guide covers releasing the `azldev` Go module so that ## TL;DR +Releases run through CI — the recommended path, with nothing to install +locally: + +1. Trigger the [**Prepare release** workflow][prepare-release-run] + (**Run workflow** → `main`). It drafts the next changelog section and pushes + a `release/vX.Y.Z` branch. +2. Wait ~30 seconds, the output summary of the workflow will generate a link to create the PR. +3. On merge, the [**release** workflow][release-run] tags `vX.Y.Z`, publishes a + GitHub Release from the changelog, and warms the proxy + pkg.go.dev — no + further action needed. + +See [Automated releases (CI)](#automated-releases-ci) for what each workflow +does. + +Local steps are: + ```console # One-time: install the changelog generator (git-cliff) cargo binstall git-cliff # or: cargo install git-cliff --locked, or: brew install git-cliff @@ -21,7 +37,8 @@ git tag -d vX.Y.Z git push origin vX.Y.Z # pushing the tag is what publishes the release ``` -Each step is explained in full under [Cut a release](#cut-a-release) below. +Each manual step is explained in full under [Cut a release](#cut-a-release) +below. ## Versioning policy @@ -76,7 +93,9 @@ pkg.go.dev fetch directly from this repository's Git tags: nothing, so the same command is safe to automate on every merge to `main`. 4. Warm the proxy and pkg.go.dev so the new version is discoverable promptly. - This is harmless and only triggers indexing of an already-public tag: + The CI release workflow does this automatically (upstream only); the commands + below are for a manual release. It is harmless — it only triggers indexing of + an already-public tag: ```console GOPROXY=https://proxy.golang.org go list \ @@ -87,9 +106,9 @@ pkg.go.dev fetch directly from this repository's Git tags: `https://pkg.go.dev/github.com/microsoft/azure-linux-dev-tools@v0.1.1` once to prompt the docs build. -5. (Optional, recommended) Create a GitHub Release for the tag and paste the new - `CHANGELOG.md` section as the release notes. A GitHub Release is separate from - the Git tag, so you can add notes even to a tag that already exists. +5. (Optional) Create a GitHub Release for the tag with that version's + `CHANGELOG.md` section as the notes. The CI release workflow does this + automatically; for a manual release, use `gh release create`. ## Changelog @@ -123,6 +142,26 @@ cargo binstall git-cliff # or: cargo install git-cliff --locked, or: brew inst > Tip: the generated draft is a natural place to let Copilot help rewrite commit > subjects into concise, user-facing notes before you commit. +## Automated releases (CI) + +Two workflows automate the manual steps above, reusing the same mage targets so +there is no second code path: + +* [`prepare-release.yml`](../../../.github/workflows/prepare-release.yml) + (manual **Run workflow**): checks out `main`, installs the pinned git-cliff, + runs `mage changelog`, and pushes a `release/vX.Y.Z` branch. It does **not** + open the PR — open it yourself from that branch, curate the draft, and merge. +* [`release.yml`](../../../.github/workflows/release.yml) (on push to `main`): + runs `mage release`, pushes the tag, publishes a GitHub Release whose notes are + that version's `CHANGELOG.md` section, and (on the upstream repo) warms the + module proxy and pkg.go.dev. It is a no-op on ordinary merges because the + changelog's top version is already tagged; it only releases when a release PR + bumps the changelog to a new version. + +Both push with the default `GITHUB_TOKEN` (`contents: write`) — no PAT needed. +A tag pushed by `GITHUB_TOKEN` does not itself trigger further workflows, which +only matters if a tag-triggered build is added later. + ## Fixing a bad release Proxy versions are immutable — you cannot delete or move a published version. @@ -141,3 +180,5 @@ retract ( [semver]: https://semver.org/ [proxy]: https://proxy.golang.org/ [retract]: https://go.dev/ref/mod#go-mod-file-retract +[prepare-release-run]: https://github.com/microsoft/azure-linux-dev-tools/actions/workflows/prepare-release.yml +[release-run]: https://github.com/microsoft/azure-linux-dev-tools/actions/workflows/release.yml From 8baf12c573bb0ff5cab4a4787b349e64cb4fd81c Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Fri, 26 Jun 2026 16:55:07 -0700 Subject: [PATCH 2/8] fixup! chore(release): add release workflows --- .github/workflows/release.yml | 40 +++++++++++++++++++----------- docs/developer/how-to/releasing.md | 4 +-- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5905eae1..7e48859b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,22 +50,34 @@ jobs: exit 1 fi tag="v${version}" + + # Push the tag if the remote lacks it. Tag push is kept separate from publishing the + # release so a rerun can finish a half-done release that pushed the tag but never + # published (e.g. a prior run that failed between the two steps). if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then - echo "Tag \`${tag}\` already on remote; nothing to release." >> "$GITHUB_STEP_SUMMARY" + echo "Tag \`${tag}\` already on remote." >> "$GITHUB_STEP_SUMMARY" else git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "refs/tags/${tag}" - # Release notes = this version's changelog section (its body, without the heading line). - awk '/^## \[/{n++; next} n==1' CHANGELOG.md > "${RUNNER_TEMP}/release-notes.md" - gh release create "${tag}" --repo "${REPO}" --title "${tag}" --notes-file "${RUNNER_TEMP}/release-notes.md" - echo "Published release \`${tag}\` with notes from CHANGELOG.md." >> "$GITHUB_STEP_SUMMARY" + echo "Pushed tag \`${tag}\`." >> "$GITHUB_STEP_SUMMARY" + fi + + # If the GitHub Release already exists, there's nothing left to publish or warm. + if gh release view "${tag}" --repo "${REPO}" >/dev/null 2>&1; then + echo "Release \`${tag}\` already exists; nothing to publish." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # Release notes = this version's changelog section (its body, without the heading line). + awk '/^## \[/{n++; next} n==1' CHANGELOG.md > "${RUNNER_TEMP}/release-notes.md" + gh release create "${tag}" --repo "${REPO}" --title "${tag}" --notes-file "${RUNNER_TEMP}/release-notes.md" + echo "Published release \`${tag}\` with notes from CHANGELOG.md." >> "$GITHUB_STEP_SUMMARY" - # Warm the module proxy and pkg.go.dev so the new version is discoverable promptly. - # Upstream only: a fork declares the upstream module path in go.mod, so the proxy would - # reject a request under the fork's path -- warming there just errors. Best-effort. - if [ "${REPO}" = "microsoft/azure-linux-dev-tools" ]; then - module="$(awk '/^module /{print $2; exit}' go.mod)" - GOPROXY=https://proxy.golang.org go list -m "${module}@${tag}" \ - || echo "proxy warm deferred; it will index on first use" - curl -fsS -o /dev/null "https://pkg.go.dev/${module}@${tag}" || true - fi + # Warm the module proxy and pkg.go.dev so the new version is discoverable promptly. + # Upstream only: a fork declares the upstream module path in go.mod, so the proxy would + # reject a request under the fork's path -- warming there just errors. Best-effort. + if [ "${REPO}" = "microsoft/azure-linux-dev-tools" ]; then + module="$(awk '/^module /{print $2; exit}' go.mod)" + GOPROXY=https://proxy.golang.org go list -m "${module}@${tag}" \ + || echo "proxy warm deferred; it will index on first use" + curl -fsS -o /dev/null "https://pkg.go.dev/${module}@${tag}" || true fi diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index fd44b2f8..0f761cf1 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -99,11 +99,11 @@ pkg.go.dev fetch directly from this repository's Git tags: ```console GOPROXY=https://proxy.golang.org go list \ - -m github.com/microsoft/azure-linux-dev-tools@v0.1.1 + -m github.com/microsoft/azure-linux-dev-tools@vX.Y.Z ``` Then visit - `https://pkg.go.dev/github.com/microsoft/azure-linux-dev-tools@v0.1.1` once to + `https://pkg.go.dev/github.com/microsoft/azure-linux-dev-tools@vX.Y.Z` once to prompt the docs build. 5. (Optional) Create a GitHub Release for the tag with that version's From bb6a46f949e22f049c12838dff5f2658200db6b1 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Tue, 30 Jun 2026 17:09:12 -0700 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/prepare-release.yml | 3 ++- .github/workflows/release.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9c679b38..b9536e84 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -89,7 +89,8 @@ jobs: git switch -c "$branch" git add CHANGELOG.md git commit -m "chore: prepare release v${version}" - git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$branch" + gh auth setup-git + git push origin "$branch" # Build the create-PR URL from the current server/repo so it points at this repo (fork or # upstream). GitHub's pull/new/ form targets the repo's default branch as the base. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e48859b..5b0012be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,8 @@ jobs: if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then echo "Tag \`${tag}\` already on remote." >> "$GITHUB_STEP_SUMMARY" else - git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "refs/tags/${tag}" + gh auth setup-git + git push origin "refs/tags/${tag}" echo "Pushed tag \`${tag}\`." >> "$GITHUB_STEP_SUMMARY" fi From b0622344470b4c42d31ca231b37008a1758b6981 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Wed, 1 Jul 2026 13:30:19 -0700 Subject: [PATCH 4/8] fixup! Apply suggestions from code review --- .github/getgotools/action.yml | 26 +++++++------- .github/workflows/prepare-release.yml | 22 +++++++++--- .github/workflows/release.yml | 23 ++++++------ docs/developer/how-to/releasing.md | 50 +++++++++++++++++---------- 4 files changed, 73 insertions(+), 48 deletions(-) diff --git a/.github/getgotools/action.yml b/.github/getgotools/action.yml index 1dca0461..3baabe2e 100644 --- a/.github/getgotools/action.yml +++ b/.github/getgotools/action.yml @@ -1,15 +1,17 @@ -name: 'Get go tools' -description: 'Install go and related go tools' +name: "Get go tools" +description: "Install go and related go tools" runs: - using: 'composite' + using: "composite" steps: - - name: Install go - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 - with: - go-version-file: 'go.mod' + - name: Install go + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version-file: "go.mod" - - name: Build and install mage - shell: bash - run: | - echo "Building and installing mage..." - go install github.com/magefile/mage@latest + - name: Build and install mage + shell: bash + run: | + echo "Building and installing mage..." + # No @version: inside the module this installs the mage version pinned in + # go.mod, so Dependabot keeps it current from a single source of truth. + go install github.com/magefile/mage diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index b9536e84..2191493b 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -24,6 +24,9 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + # Always draft from the repo's default branch (main upstream), not the + # branch this workflow_dispatch happened to be launched from. + ref: ${{ github.event.repository.default_branch }} # git-cliff needs full history and tags to compute the next version. fetch-depth: 0 persist-credentials: false @@ -70,17 +73,28 @@ jobs: exit 0 fi - version="$(sed -n 's/^## \[\([0-9.]*\)\].*/\1/p' CHANGELOG.md | head -1)" + version="$(sed -n 's/^## \[\([0-9]\+\.[0-9]\+\.[0-9]\+\)\].*/\1/p' CHANGELOG.md | head -1)" if [ -z "$version" ]; then echo "no '## [X.Y.Z]' heading found in CHANGELOG.md" >&2 exit 1 fi branch="release/v${version}" - # Refuse to clobber an existing release branch (e.g. a prior prepare-release run that - # has not been merged or cleaned up yet). + # A release branch for this version already exists (e.g. a prior prepare-release run + # that hasn't been merged or cleaned up). Fail rather than clobber it -- but leave a + # summary with the PR link so recovery is a click away. ($GITHUB_STEP_SUMMARY still + # renders even though the step exits non-zero.) if git ls-remote --exit-code --heads origin "refs/heads/${branch}" >/dev/null 2>&1; then - echo "Release branch '${branch}' already exists on the remote; merge or delete it before preparing again." >&2 + pr_url="${SERVER_URL}/${REPO}/pull/new/${branch}" + { + echo "### Release branch already exists" + echo "" + echo "Branch \`${branch}\` already exists, so no new draft was created." + echo "" + echo "**[Open its pull request →](${pr_url})** to finish the release, or delete the" + echo "branch and re-run this workflow to regenerate the draft from the latest \`main\`." + } >> "$GITHUB_STEP_SUMMARY" + echo "Release branch '${branch}' already exists; open its PR or delete it and re-run." >&2 exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b0012be..04f02576 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: REPO: ${{ github.repository }} run: | set -euo pipefail - version="$(sed -n 's/^## \[\([0-9.]*\)\].*/\1/p' CHANGELOG.md | head -1)" + version="$(sed -n 's/^## \[\([0-9]\+\.[0-9]\+\.[0-9]\+\)\].*/\1/p' CHANGELOG.md | head -1)" if [ -z "$version" ]; then echo "no '## [X.Y.Z]' heading found in CHANGELOG.md" >&2 exit 1 @@ -69,16 +69,13 @@ jobs: fi # Release notes = this version's changelog section (its body, without the heading line). - awk '/^## \[/{n++; next} n==1' CHANGELOG.md > "${RUNNER_TEMP}/release-notes.md" - gh release create "${tag}" --repo "${REPO}" --title "${tag}" --notes-file "${RUNNER_TEMP}/release-notes.md" - echo "Published release \`${tag}\` with notes from CHANGELOG.md." >> "$GITHUB_STEP_SUMMARY" - - # Warm the module proxy and pkg.go.dev so the new version is discoverable promptly. - # Upstream only: a fork declares the upstream module path in go.mod, so the proxy would - # reject a request under the fork's path -- warming there just errors. Best-effort. - if [ "${REPO}" = "microsoft/azure-linux-dev-tools" ]; then - module="$(awk '/^module /{print $2; exit}' go.mod)" - GOPROXY=https://proxy.golang.org go list -m "${module}@${tag}" \ - || echo "proxy warm deferred; it will index on first use" - curl -fsS -o /dev/null "https://pkg.go.dev/${module}@${tag}" || true + # Match only numeric version headings so this stays in sync with the version parsed + # above and can never capture a non-release heading. + notes="${RUNNER_TEMP}/release-notes.md" + awk '/^## \[[0-9]/{n++; next} n==1' CHANGELOG.md > "${notes}" + if [ ! -s "${notes}" ]; then + echo "no changelog notes found for ${tag}" >&2 + exit 1 fi + gh release create "${tag}" --repo "${REPO}" --title "${tag}" --notes-file "${notes}" + echo "Published release \`${tag}\` with notes from CHANGELOG.md." >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index 0f761cf1..da7ea7c0 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -12,9 +12,8 @@ locally: (**Run workflow** → `main`). It drafts the next changelog section and pushes a `release/vX.Y.Z` branch. 2. Wait ~30 seconds, the output summary of the workflow will generate a link to create the PR. -3. On merge, the [**release** workflow][release-run] tags `vX.Y.Z`, publishes a - GitHub Release from the changelog, and warms the proxy + pkg.go.dev — no - further action needed. +3. On merge, the [**release** workflow][release-run] tags `vX.Y.Z` and publishes + a GitHub Release from the changelog — no further action needed. See [Automated releases (CI)](#automated-releases-ci) for what each workflow does. @@ -92,10 +91,9 @@ pkg.go.dev fetch directly from this repository's Git tags: `mage release` is idempotent: if that version is already tagged it does nothing, so the same command is safe to automate on every merge to `main`. -4. Warm the proxy and pkg.go.dev so the new version is discoverable promptly. - The CI release workflow does this automatically (upstream only); the commands - below are for a manual release. It is harmless — it only triggers indexing of - an already-public tag: +4. (Optional) The proxy and pkg.go.dev index a new version on the first request, + so nothing is required here. To make a release discoverable immediately, warm + them by hand — this only triggers indexing of an already-public tag: ```console GOPROXY=https://proxy.golang.org go list \ @@ -148,15 +146,15 @@ Two workflows automate the manual steps above, reusing the same mage targets so there is no second code path: * [`prepare-release.yml`](../../../.github/workflows/prepare-release.yml) - (manual **Run workflow**): checks out `main`, installs the pinned git-cliff, - runs `mage changelog`, and pushes a `release/vX.Y.Z` branch. It does **not** - open the PR — open it yourself from that branch, curate the draft, and merge. + (manual **Run workflow**): checks out the repo's default branch (`main`), + installs the pinned git-cliff, runs `mage changelog`, and pushes a + `release/vX.Y.Z` branch. It does **not** open the PR — open it yourself from + that branch, curate the draft, and merge. * [`release.yml`](../../../.github/workflows/release.yml) (on push to `main`): - runs `mage release`, pushes the tag, publishes a GitHub Release whose notes are - that version's `CHANGELOG.md` section, and (on the upstream repo) warms the - module proxy and pkg.go.dev. It is a no-op on ordinary merges because the - changelog's top version is already tagged; it only releases when a release PR - bumps the changelog to a new version. + runs `mage release`, pushes the tag, and publishes a GitHub Release whose notes + are that version's `CHANGELOG.md` section. It is a no-op on ordinary merges + because the changelog's top version is already tagged; it only releases when a + release PR bumps the changelog to a new version. Both push with the default `GITHUB_TOKEN` (`contents: write`) — no PAT needed. A tag pushed by `GITHUB_TOKEN` does not itself trigger further workflows, which @@ -164,10 +162,24 @@ only matters if a tag-triggered build is added later. ## Fixing a bad release -Proxy versions are immutable — you cannot delete or move a published version. -To withdraw one, [retract][retract] it: add a `retract` directive to `go.mod` -describing the bad version(s) and release a new patch. `go get` will then skip -the retracted versions. +### If the release workflow fails partway + +The [release workflow][release-run] is idempotent: it skips the tag push and the +GitHub Release when they already exist, so the fix is usually just to **re-run it +before pushing anything else to `main`**. The tag is created on whatever commit +is at the top of `main` at run time, so if an unrelated commit lands before you +re-run, the tag could point at it instead of the release commit. If that happens +— and no one has fetched `module@vX.Y.Z` through the proxy yet — delete the +remote tag and GitHub Release, then re-run. Once the proxy has served the +version it is immutable; retract it instead (below). + +### Withdrawing a published version + +A version becomes immutable the first time the [Go module proxy][proxy] serves +`module@vX.Y.Z` — after that you cannot delete or move it. To withdraw one, +[retract][retract] it: add a `retract` directive to `go.mod` describing the bad +version(s) and release a new patch. `go get` will then skip the retracted +versions. ```go // in go.mod From 8cb4bd054624b2b1f4b9d49b37d82b1738185278 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Wed, 1 Jul 2026 14:11:30 -0700 Subject: [PATCH 5/8] fixup! Apply suggestions from code review --- docs/developer/how-to/releasing.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index da7ea7c0..bb4f8b14 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -33,7 +33,11 @@ mage release # creates annotated tag vX.Y.Z on HEAD (does no # Undo a local tag created by mistake (before pushing) git tag -d vX.Y.Z -git push origin vX.Y.Z # pushing the tag is what publishes the release +git push origin vX.Y.Z # pushing the tag publishes the version to the Go proxy + +# 3. Create the GitHub Release with this version's changelog notes +gh release create vX.Y.Z --title vX.Y.Z \ + --notes-file <(awk '/^## \[[0-9]/{n++; next} n==1' CHANGELOG.md) ``` Each manual step is explained in full under [Cut a release](#cut-a-release) From 7870af7f56f510abdd7d38a7fd51d2ff94289c67 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Wed, 1 Jul 2026 14:15:07 -0700 Subject: [PATCH 6/8] fixup! Apply suggestions from code review --- .github/workflows/release.yml | 11 +++++++---- docs/developer/how-to/releasing.md | 11 ++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04f02576..1e1865cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,15 @@ name: "Release" -# Runs on every push to main. When CHANGELOG.md carries a new version, tags it -# (via the idempotent `mage release`), pushes the tag, and publishes a GitHub -# Release with that version's changelog notes. Normal merges are a no-op because -# the changelog's top version is already tagged. See docs/developer/how-to/releasing.md. +# Runs when a push to main changes CHANGELOG.md -- i.e. when a release PR merges. +# Tags the new version via the idempotent `mage release`, pushes the tag, and +# publishes a GitHub Release with that version's changelog notes. Still idempotent: +# if the top version is already tagged (e.g. a later CHANGELOG.md edit) it's a +# no-op. See docs/developer/how-to/releasing.md. on: push: branches: [main] + paths: + - CHANGELOG.md # Don't run two releases at once. concurrency: diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index bb4f8b14..055a4f9a 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -154,11 +154,12 @@ there is no second code path: installs the pinned git-cliff, runs `mage changelog`, and pushes a `release/vX.Y.Z` branch. It does **not** open the PR — open it yourself from that branch, curate the draft, and merge. -* [`release.yml`](../../../.github/workflows/release.yml) (on push to `main`): - runs `mage release`, pushes the tag, and publishes a GitHub Release whose notes - are that version's `CHANGELOG.md` section. It is a no-op on ordinary merges - because the changelog's top version is already tagged; it only releases when a - release PR bumps the changelog to a new version. +* [`release.yml`](../../../.github/workflows/release.yml) (on pushes to `main` + that change `CHANGELOG.md`): runs `mage release`, pushes the tag, and publishes + a GitHub Release whose notes are that version's `CHANGELOG.md` section. The path + filter keeps ordinary merges from triggering it; it also stays idempotent — a + `CHANGELOG.md` edit that doesn't bump the version is a no-op because the top + version is already tagged. Both push with the default `GITHUB_TOKEN` (`contents: write`) — no PAT needed. A tag pushed by `GITHUB_TOKEN` does not itself trigger further workflows, which From 6c38614adc591031d17bccfa1e38d8653159a653 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Wed, 1 Jul 2026 14:39:41 -0700 Subject: [PATCH 7/8] fixup! Apply suggestions from code review --- docs/developer/how-to/releasing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index 055a4f9a..e840922b 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -12,7 +12,8 @@ locally: (**Run workflow** → `main`). It drafts the next changelog section and pushes a `release/vX.Y.Z` branch. 2. Wait ~30 seconds, the output summary of the workflow will generate a link to create the PR. -3. On merge, the [**release** workflow][release-run] tags `vX.Y.Z` and publishes +3. Edit the draft changelog into user-facing notes, then merge the PR to `main`. Consider using '@copilot Update the new changelog section into user-facing release notes' in GitHub to help rewrite the changelog. +4. On merge, the [**release** workflow][release-run] tags `vX.Y.Z` and publishes a GitHub Release from the changelog — no further action needed. See [Automated releases (CI)](#automated-releases-ci) for what each workflow From 3e5f6f53e99d1ee7e90ebe8f1ccfc97f317cd083 Mon Sep 17 00:00:00 2001 From: Daniel McIlvaney Date: Thu, 2 Jul 2026 11:21:16 -0700 Subject: [PATCH 8/8] fixup! Apply suggestions from code review --- .github/workflows/prepare-release.yml | 2 +- .github/workflows/release.yml | 7 ++++--- docs/developer/how-to/releasing.md | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 2191493b..1eb67648 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -73,7 +73,7 @@ jobs: exit 0 fi - version="$(sed -n 's/^## \[\([0-9]\+\.[0-9]\+\.[0-9]\+\)\].*/\1/p' CHANGELOG.md | head -1)" + version="$(sed -n 's/^##[[:space:]]\+\[\([0-9]\+\.[0-9]\+\.[0-9]\+\)\].*/\1/p' CHANGELOG.md | head -1)" if [ -z "$version" ]; then echo "no '## [X.Y.Z]' heading found in CHANGELOG.md" >&2 exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e1865cf..45b2702f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: REPO: ${{ github.repository }} run: | set -euo pipefail - version="$(sed -n 's/^## \[\([0-9]\+\.[0-9]\+\.[0-9]\+\)\].*/\1/p' CHANGELOG.md | head -1)" + version="$(sed -n 's/^##[[:space:]]\+\[\([0-9]\+\.[0-9]\+\.[0-9]\+\)\].*/\1/p' CHANGELOG.md | head -1)" if [ -z "$version" ]; then echo "no '## [X.Y.Z]' heading found in CHANGELOG.md" >&2 exit 1 @@ -73,9 +73,10 @@ jobs: # Release notes = this version's changelog section (its body, without the heading line). # Match only numeric version headings so this stays in sync with the version parsed - # above and can never capture a non-release heading. + # above and can never capture a non-release heading. Tolerate any whitespace after '##' + # to match `mage release`. notes="${RUNNER_TEMP}/release-notes.md" - awk '/^## \[[0-9]/{n++; next} n==1' CHANGELOG.md > "${notes}" + awk '/^##[[:space:]]+\[[0-9]/{n++; next} n==1' CHANGELOG.md > "${notes}" if [ ! -s "${notes}" ]; then echo "no changelog notes found for ${tag}" >&2 exit 1 diff --git a/docs/developer/how-to/releasing.md b/docs/developer/how-to/releasing.md index e840922b..b02d0d28 100644 --- a/docs/developer/how-to/releasing.md +++ b/docs/developer/how-to/releasing.md @@ -38,7 +38,7 @@ git push origin vX.Y.Z # pushing the tag publishes the version to the # 3. Create the GitHub Release with this version's changelog notes gh release create vX.Y.Z --title vX.Y.Z \ - --notes-file <(awk '/^## \[[0-9]/{n++; next} n==1' CHANGELOG.md) + --notes-file <(awk '/^##[[:space:]]+\[[0-9]/{n++; next} n==1' CHANGELOG.md) ``` Each manual step is explained in full under [Cut a release](#cut-a-release)