Skip to content
Merged
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
180 changes: 180 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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