Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions .github/getgotools/action.yml
Original file line number Diff line number Diff line change
@@ -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
117 changes: 117 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remind me who is allowed to run this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need write perms, and to be in the collaborator group or better.
I wasn't too worried about this, since all it can do is create a branch, you still need to make the PR, get it approved, and merge it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are ways to filter in the workflow itself if needed.


# 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:
# 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

- 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/^##[[: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
fi
branch="release/v${version}"

# 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
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

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}"
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/<branch> 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}"
85 changes: 85 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: "Release"

# 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]
Comment thread
dmcilvaney marked this conversation as resolved.
paths:
- CHANGELOG.md

# 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/^##[[: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
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." >> "$GITHUB_STEP_SUMMARY"
else
gh auth setup-git
git push origin "refs/tags/${tag}"
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).
# Match only numeric version headings so this stays in sync with the version parsed
# above and can never capture a non-release heading. Tolerate any whitespace after '##'
# to match `mage release`.
notes="${RUNNER_TEMP}/release-notes.md"
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
fi
gh release create "${tag}" --repo "${REPO}" --title "${tag}" --notes-file "${notes}"
echo "Published release \`${tag}\` with notes from CHANGELOG.md." >> "$GITHUB_STEP_SUMMARY"
85 changes: 72 additions & 13 deletions docs/developer/how-to/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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
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
Expand All @@ -18,10 +34,15 @@ 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 '/^##[[:space:]]+\[[0-9]/{n++; next} n==1' CHANGELOG.md)
```

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

Expand Down Expand Up @@ -75,21 +96,22 @@ 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.
This is harmless and 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 \
-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, 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

Expand Down Expand Up @@ -123,12 +145,47 @@ 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 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 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
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
Expand All @@ -141,3 +198,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
Loading