Skip to content

Commit 96529a1

Browse files
authored
Add automated semver + CI + binary release workflows (#25)
* Adopt pizzabot's automated semver + CI + binary release workflows CI runs `go vet`, `go build`, `go test -race` on every PR and push to main. A successful run on main triggers the release workflow, which reads the merged PR's `release:*` label (default `patch`), computes the next semver tag, builds a static linux/amd64 binary, pushes the tag, and publishes a GitHub release with auto-generated notes. Replaces the previous tag-triggered release in `go.yml` with a fully automated PR-label-driven flow. * Drop go.yml superseded by ci.yml + release.yml The tag-triggered build/release flow is replaced by the PR-label-driven release.yml. * Scrub project-name reference from CI workflow comment
1 parent 803a39d commit 96529a1

3 files changed

Lines changed: 188 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
# Build, vet, and test the Go binary on every PR and push to main.
4+
# The release workflow chains off of a successful run.
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches: [main]
10+
11+
jobs:
12+
test:
13+
name: build & test
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
- uses: actions/setup-go@v6
19+
with:
20+
go-version-file: go.mod
21+
cache: true
22+
23+
- name: go vet
24+
run: go vet ./...
25+
26+
- name: go build
27+
run: go build ./...
28+
29+
- name: go test
30+
run: go test -race ./...

.github/workflows/go.yml

Lines changed: 0 additions & 40 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
name: release
2+
3+
# Continuous release. After the CI workflow finishes successfully on
4+
# `main` (i.e. immediately after a merge), this workflow reads the
5+
# merged PR's `release:*` label, computes the next semver tag, and
6+
# publishes a git tag + GitHub release. Tag format is `vMAJOR.MINOR.PATCH`
7+
# — the convention Go modules require, used by `goreleaser`,
8+
# `release-please`, `semantic-release`, and most published Go libraries.
9+
#
10+
# Version-bump labels (apply exactly one on the PR; default `release:patch`):
11+
#
12+
# release:major → 1.4.7 → 2.0.0 Breaking change.
13+
# release:minor → 1.4.7 → 1.5.0 Backwards-compatible new behavior.
14+
# release:patch → 1.4.7 → 1.4.8 Bug fix, refactor, dependency bump.
15+
# Default when nothing else is set.
16+
# release:skip → no release Docs-only / CI-only change.
17+
#
18+
# Precedence when more than one label is present: skip > major > minor >
19+
# patch. `release:skip` always wins so a PR can be parked mid-flight;
20+
# otherwise the largest declared bump wins.
21+
#
22+
# Direct pushes to `main` (no PR) fall back to `release:patch`.
23+
24+
on:
25+
workflow_run:
26+
workflows: [CI]
27+
types: [completed]
28+
branches: [main]
29+
30+
concurrency:
31+
group: release
32+
cancel-in-progress: false
33+
34+
permissions:
35+
contents: write # push tags, create releases
36+
pull-requests: read # read the merged PR's labels
37+
38+
jobs:
39+
release:
40+
# Only act on a successful CI run that was itself triggered by a push
41+
# to main (i.e. the post-merge run, not the PR-time run).
42+
if: >-
43+
github.event.workflow_run.conclusion == 'success' &&
44+
github.event.workflow_run.event == 'push'
45+
runs-on: ubuntu-latest
46+
timeout-minutes: 10
47+
env:
48+
GH_TOKEN: ${{ github.token }}
49+
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
50+
steps:
51+
- uses: actions/checkout@v6
52+
with:
53+
ref: ${{ github.event.workflow_run.head_sha }}
54+
fetch-depth: 0 # full history + tags for semver math
55+
56+
- name: Resolve merged PR and version bump
57+
id: bump
58+
run: |
59+
set -euo pipefail
60+
pr_json="$(gh api "repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" --jq '.[0] // empty')"
61+
if [[ -z "$pr_json" ]]; then
62+
echo "No PR found for ${HEAD_SHA} — direct push to main. Defaulting to release:patch."
63+
echo "bump=patch" >> "$GITHUB_OUTPUT"
64+
echo "pr=" >> "$GITHUB_OUTPUT"
65+
exit 0
66+
fi
67+
68+
number="$(jq -r '.number' <<<"$pr_json")"
69+
labels="$(jq -r '.labels[].name' <<<"$pr_json")"
70+
echo "PR #${number} labels:"
71+
printf ' %s\n' $labels
72+
73+
# Precedence: skip > major > minor > patch (default).
74+
if grep -qx 'release:skip' <<<"$labels"; then
75+
bump=skip
76+
elif grep -qx 'release:major' <<<"$labels"; then
77+
bump=major
78+
elif grep -qx 'release:minor' <<<"$labels"; then
79+
bump=minor
80+
else
81+
bump=patch
82+
fi
83+
84+
echo "Resolved bump: ${bump}"
85+
echo "bump=${bump}" >> "$GITHUB_OUTPUT"
86+
echo "pr=${number}" >> "$GITHUB_OUTPUT"
87+
88+
- name: Skip release
89+
if: steps.bump.outputs.bump == 'skip'
90+
run: |
91+
echo "release:skip on PR #${{ steps.bump.outputs.pr }} — no tag created."
92+
93+
- name: Compute next semver tag
94+
if: steps.bump.outputs.bump != 'skip'
95+
id: tag
96+
run: |
97+
set -euo pipefail
98+
git fetch --tags --quiet
99+
latest="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)"
100+
if [[ -z "$latest" ]]; then
101+
latest="v0.0.0"
102+
fi
103+
IFS='.' read -r major minor patch <<<"${latest#v}"
104+
case "${{ steps.bump.outputs.bump }}" in
105+
major) major=$((major+1)); minor=0; patch=0 ;;
106+
minor) minor=$((minor+1)); patch=0 ;;
107+
patch) patch=$((patch+1)) ;;
108+
esac
109+
next="v${major}.${minor}.${patch}"
110+
echo "Bumping ${latest} → ${next} (${{ steps.bump.outputs.bump }})"
111+
echo "latest=${latest}" >> "$GITHUB_OUTPUT"
112+
echo "next=${next}" >> "$GITHUB_OUTPUT"
113+
114+
- name: Install Go
115+
if: steps.bump.outputs.bump != 'skip'
116+
uses: actions/setup-go@v6
117+
with:
118+
go-version-file: go.mod
119+
cache: true
120+
121+
- name: Build release binary
122+
if: steps.bump.outputs.bump != 'skip'
123+
# Static linux/amd64 binary. CGO off so the binary has no glibc
124+
# dependency; -trimpath + -s -w shrinks size and strips local paths.
125+
run: |
126+
set -euo pipefail
127+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
128+
go build -trimpath -ldflags="-s -w" \
129+
-o find-replace-linux-amd64 .
130+
ls -l find-replace-linux-amd64
131+
132+
- name: Create and push tag
133+
if: steps.bump.outputs.bump != 'skip'
134+
run: |
135+
set -euo pipefail
136+
tag="${{ steps.tag.outputs.next }}"
137+
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
138+
echo "Tag ${tag} already exists locally — skipping." && exit 0
139+
fi
140+
git config user.name 'github-actions[bot]'
141+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
142+
git tag -a "${tag}" "${HEAD_SHA}" -m "${tag}"
143+
git push origin "${tag}"
144+
145+
- name: Publish GitHub release
146+
if: steps.bump.outputs.bump != 'skip'
147+
run: |
148+
set -euo pipefail
149+
tag="${{ steps.tag.outputs.next }}"
150+
prev="${{ steps.tag.outputs.latest }}"
151+
notes_args=(--generate-notes)
152+
if [[ "${prev}" != "v0.0.0" ]]; then
153+
notes_args+=(--notes-start-tag "${prev}")
154+
fi
155+
gh release create "${tag}" find-replace-linux-amd64 \
156+
--target "${HEAD_SHA}" \
157+
--title "${tag}" \
158+
"${notes_args[@]}"

0 commit comments

Comments
 (0)