Skip to content

Release

Release #1

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
dry_run:
description: 'Dry run (skip tag push, release creation, and proxy trigger)'
required: false
type: boolean
default: false
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
fetch-tags: true
- name: Authorize release actor
run: |
ACTOR="${{ github.actor }}"
ALLOWED=""
if [ -f CODEOWNERS ]; then
ALLOWED="$ALLOWED $(grep -oP '@\K[\w-]+' CODEOWNERS | sort -u)"
fi
if [ -f RELEASERS ]; then
ALLOWED="$ALLOWED $(grep -vE '^\s*(#|$)' RELEASERS | tr -s '[:space:]' ' ')"
fi
for user in $ALLOWED; do
if [ "$ACTOR" = "$user" ]; then
echo "Authorized: $ACTOR is in CODEOWNERS or RELEASERS"
exit 0
fi
done
echo "::error::$ACTOR is not authorized to create releases. Only users listed in CODEOWNERS or RELEASERS may trigger this workflow."
exit 1
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Determine next version
id: version
run: |
LATEST=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
if [ -z "$LATEST" ]; then
LATEST="v0.0.0"
fi
echo "current=$LATEST" >> "$GITHUB_OUTPUT"
MAJOR=$(echo "$LATEST" | sed 's/^v//' | cut -d. -f1)
MINOR=$(echo "$LATEST" | sed 's/^v//' | cut -d. -f2)
PATCH=$(echo "$LATEST" | sed 's/^v//' | cut -d. -f3)
case "${{ inputs.bump }}" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
NEXT="v${MAJOR}.${MINOR}.${PATCH}"
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
echo "### Version bump: $LATEST → $NEXT (${{ inputs.bump }})" >> "$GITHUB_STEP_SUMMARY"
- name: Verify CI passed on HEAD
run: |
HEAD_SHA=$(git rev-parse HEAD)
echo "Checking CI status for $HEAD_SHA..."
CONCLUSION=$(gh run list --commit "$HEAD_SHA" --workflow ci.yml --json conclusion --jq '.[0].conclusion // "none"')
if [ "$CONCLUSION" != "success" ]; then
echo "::error::CI has not passed on HEAD ($HEAD_SHA). Latest conclusion: $CONCLUSION"
exit 1
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify CHANGELOG has entry for next version
run: |
NEXT="${{ steps.version.outputs.next }}"
if ! grep -q "## $NEXT" CHANGELOG.md; then
echo "::error::CHANGELOG.md does not contain an entry for $NEXT. Add the changelog entry before releasing."
exit 1
fi
- name: Build and test
run: |
make build
make test-race
- name: Extract changelog for release notes
id: notes
run: |
NEXT="${{ steps.version.outputs.next }}"
NOTES=$(awk "/^## $NEXT/{flag=1; next} /^## v[0-9]/{flag=0} flag" CHANGELOG.md)
{
echo "body<<RELEASE_NOTES_EOF"
echo "$NOTES"
echo "RELEASE_NOTES_EOF"
} >> "$GITHUB_OUTPUT"
- name: Create annotated tag
if: ${{ !inputs.dry_run }}
run: |
NEXT="${{ steps.version.outputs.next }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$NEXT" -m "$NEXT"
git push origin "$NEXT"
- name: Create GitHub release
if: ${{ !inputs.dry_run }}
run: |
NEXT="${{ steps.version.outputs.next }}"
gh release create "$NEXT" \
--title "$NEXT" \
--notes "$RELEASE_NOTES" \
--verify-tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NOTES: ${{ steps.notes.outputs.body }}
- name: Trigger Go module proxy indexing
if: ${{ !inputs.dry_run }}
run: |
NEXT="${{ steps.version.outputs.next }}"
MODULE=$(head -1 go.mod | awk '{print $2}')
echo "Requesting proxy indexing for ${MODULE}@${NEXT}..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://proxy.golang.org/${MODULE}/@v/${NEXT}.info")
echo "Proxy response: $HTTP_CODE"
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Go module proxy returned $HTTP_CODE — indexing may be delayed"
fi
- name: Verify pkg.go.dev availability
if: ${{ !inputs.dry_run }}
run: |
NEXT="${{ steps.version.outputs.next }}"
MODULE=$(head -1 go.mod | awk '{print $2}')
echo "Waiting for pkg.go.dev to index ${MODULE}@${NEXT}..."
for i in 1 2 3 4 5; do
sleep 15
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://pkg.go.dev/${MODULE}@${NEXT}")
echo "Attempt $i: HTTP $HTTP_CODE"
if [ "$HTTP_CODE" -eq 200 ]; then
echo "pkg.go.dev is serving ${MODULE}@${NEXT}"
echo "### pkg.go.dev: [${MODULE}@${NEXT}](https://pkg.go.dev/${MODULE}@${NEXT})" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
done
echo "::warning::pkg.go.dev has not indexed ${NEXT} yet — check manually at https://pkg.go.dev/${MODULE}@${NEXT}"
- name: Summary
run: |
NEXT="${{ steps.version.outputs.next }}"
CURRENT="${{ steps.version.outputs.current }}"
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "### Dry run complete" >> "$GITHUB_STEP_SUMMARY"
echo "Would have tagged **$NEXT** (from $CURRENT, ${{ inputs.bump }} bump)" >> "$GITHUB_STEP_SUMMARY"
else
echo "### Release $NEXT published" >> "$GITHUB_STEP_SUMMARY"
echo "- Tag: [$NEXT](https://github.com/${{ github.repository }}/releases/tag/$NEXT)" >> "$GITHUB_STEP_SUMMARY"
echo "- Bump: $CURRENT → $NEXT (${{ inputs.bump }})" >> "$GITHUB_STEP_SUMMARY"
fi