Skip to content

Commit e7973d1

Browse files
ci: add GoReleaser, release, winget, and Homebrew tap workflows
- CI: add golangci-lint job alongside test - release.yml: tag creation + GoReleaser with Homebrew formula verify gate - release-validate.yml: validate both goreleaser configs on every push/PR - winget.yml: manual dispatch workflow for WinGet manifest publishing - .goreleaser.yaml: prerelease config (binaries only) - .goreleaser.stable.yaml: stable config with Homebrew tap + deb/rpm packages - PR template Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 472bdf7 commit e7973d1

7 files changed

Lines changed: 556 additions & 0 deletions

File tree

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## Summary
2+
3+
Describe the change you are making.
4+
5+
## Why this change?
6+
7+
Explain why this change is needed.
8+
9+
## Bug Reproducibility (if this fixes a bug)
10+
11+
- Repro steps:
12+
1.
13+
2.
14+
3.
15+
- If reproducibility steps are not feasible, explain why:
16+
17+
## Tests
18+
19+
- [ ] I added or updated tests for all code changes in this PR.
20+
- [ ] For bug fixes, I added regression coverage.
21+
- [ ] I ran `go test -race -count=1 ./...`.
22+
- [ ] I ran `go vet ./...`.
23+
24+
## Risk / Rollback Notes (optional)
25+
26+
Describe user impact, known risks, and how to roll back if needed.

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ permissions:
1010
contents: read
1111

1212
jobs:
13+
lint:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-go@v5
18+
with:
19+
go-version-file: go.mod
20+
cache: false
21+
- uses: golangci/golangci-lint-action@v7
22+
with:
23+
version: v2.11.4
24+
args: --timeout=5m
25+
1326
test:
1427
runs-on: ubuntu-latest
1528
steps:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Release Config Validation
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
goreleaser-config:
14+
name: Validate GoReleaser Configs
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
cache: true
23+
24+
- uses: goreleaser/goreleaser-action@v6
25+
with:
26+
distribution: goreleaser
27+
version: "~> v2"
28+
args: check --config .goreleaser.yaml
29+
30+
- uses: goreleaser/goreleaser-action@v6
31+
with:
32+
distribution: goreleaser
33+
version: "~> v2"
34+
args: check --config .goreleaser.stable.yaml

.github/workflows/release.yml

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag:
7+
description: 'SemVer tag to release (e.g. v0.1.0)'
8+
required: true
9+
type: string
10+
default: 'v0.1.0'
11+
# Allows releases when workflow_dispatch is unavailable (e.g. narrow GitHub tokens):
12+
# create an annotated tag on main and push it — GoReleaser runs for that tag.
13+
push:
14+
tags:
15+
- 'v*.*.*'
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
tag:
22+
name: Create tag
23+
if: github.event_name == 'workflow_dispatch'
24+
runs-on: ubuntu-latest
25+
permissions:
26+
contents: write
27+
outputs:
28+
tag: ${{ steps.tag.outputs.tag }}
29+
steps:
30+
- uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: Require main branch
35+
run: |
36+
if [ "${GITHUB_REF}" != "refs/heads/main" ]; then
37+
echo "Release workflow must be run from main; got ${GITHUB_REF}."
38+
exit 1
39+
fi
40+
41+
- name: Validate version format
42+
env:
43+
TAG: ${{ inputs.tag }}
44+
run: |
45+
if ! echo "$TAG" | grep -qE '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then
46+
echo "Invalid tag format: $TAG"
47+
echo "Expected: vMAJOR.MINOR.PATCH"
48+
exit 1
49+
fi
50+
51+
- name: Ensure tag is reusable or create it
52+
id: tag
53+
env:
54+
TAG: ${{ inputs.tag }}
55+
run: |
56+
HEAD_SHA="$(git rev-parse HEAD)"
57+
58+
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
59+
TAG_SHA="$(git rev-list -n1 "$TAG")"
60+
echo "Tag already exists locally: $TAG ($TAG_SHA)"
61+
exit 1
62+
else
63+
REMOTE_SHA="$(git ls-remote --tags origin "refs/tags/$TAG" | awk '{print $1}' | head -n1)"
64+
if [ -n "$REMOTE_SHA" ]; then
65+
echo "Tag already exists on origin: $TAG ($REMOTE_SHA)"
66+
exit 1
67+
else
68+
git config user.name "github-actions[bot]"
69+
git config user.email "github-actions[bot]@users.noreply.github.com"
70+
git tag -a "${TAG}" -m "Release ${TAG}"
71+
git push origin "refs/tags/${TAG}"
72+
fi
73+
fi
74+
echo "tag=${TAG}" >> $GITHUB_OUTPUT
75+
76+
goreleaser:
77+
name: GoReleaser
78+
needs:
79+
- tag
80+
# Run after workflow_dispatch tagging succeeds, or when a release tag was pushed
81+
# (the tag job is skipped on push; skipped needs require always() so this if is evaluated).
82+
# always() && !cancelled(): respect workflow cancellation.
83+
# Push path: only initial tag creation, not delete/force-push.
84+
if: |
85+
always() && !cancelled() &&
86+
(github.event_name != 'push' || (github.event.created && !github.event.deleted && !github.event.forced)) &&
87+
(needs.tag.result == 'success' ||
88+
(github.event_name == 'push' && needs.tag.result == 'skipped'))
89+
runs-on: ubuntu-latest
90+
permissions:
91+
contents: write
92+
id-token: write
93+
steps:
94+
- uses: actions/checkout@v4
95+
with:
96+
fetch-depth: 0
97+
ref: >-
98+
${{
99+
github.event_name == 'workflow_dispatch'
100+
&& needs.tag.outputs.tag
101+
|| github.ref
102+
}}
103+
104+
- name: Validate pushed tag format
105+
if: github.event_name == 'push'
106+
env:
107+
TAG: ${{ github.ref_name }}
108+
run: |
109+
set -euo pipefail
110+
if ! echo "$TAG" | grep -qE '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then
111+
echo "Invalid tag format: $TAG"
112+
echo "Expected: vMAJOR.MINOR.PATCH"
113+
exit 1
114+
fi
115+
116+
- name: Determine release tag
117+
id: release_meta
118+
env:
119+
EVENT_NAME: ${{ github.event_name }}
120+
TAG_WORKFLOW_DISPATCH: ${{ needs.tag.outputs.tag }}
121+
TAG_PUSH: ${{ github.ref_name }}
122+
run: |
123+
set -euo pipefail
124+
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
125+
TAG="${TAG_WORKFLOW_DISPATCH}"
126+
else
127+
TAG="${TAG_PUSH}"
128+
fi
129+
echo "release_tag=$TAG" >> "$GITHUB_OUTPUT"
130+
131+
- name: Require pushed tag to target mainline
132+
if: github.event_name == 'push'
133+
run: |
134+
set -euo pipefail
135+
TAG_COMMIT="$(git rev-parse HEAD)"
136+
git fetch origin "refs/heads/main:refs/remotes/origin/main"
137+
if ! git merge-base --is-ancestor "$TAG_COMMIT" "origin/main"; then
138+
echo "Release tag must point to a commit reachable from origin/main (not a feature-branch-only SHA)."
139+
exit 1
140+
fi
141+
142+
- uses: actions/setup-go@v5
143+
with:
144+
go-version-file: go.mod
145+
cache: true
146+
147+
- uses: goreleaser/goreleaser-action@v6
148+
with:
149+
distribution: goreleaser
150+
version: "~> v2"
151+
args: release --clean --config .goreleaser.stable.yaml
152+
env:
153+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
154+
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
155+
156+
- name: Verify Homebrew formula publish
157+
env:
158+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159+
RELEASE_TAG: ${{ steps.release_meta.outputs.release_tag }}
160+
run: |
161+
set -euo pipefail
162+
RELEASE_VERSION="${RELEASE_TAG#v}"
163+
for attempt in $(seq 1 18); do
164+
FORMULA_B64="$(gh api repos/git-rain/homebrew-tap/contents/Formula/git-rain.rb --jq '.content' 2>/dev/null || true)"
165+
if [ -n "$FORMULA_B64" ]; then
166+
FORMULA_CONTENT="$(printf '%s' "$FORMULA_B64" | tr -d '\n' | base64 -d)"
167+
if printf '%s' "$FORMULA_CONTENT" | grep -q "version \"${RELEASE_VERSION}\"" && \
168+
printf '%s' "$FORMULA_CONTENT" | grep -q "/download/${RELEASE_TAG}/"; then
169+
echo "Homebrew formula updated for ${RELEASE_TAG}."
170+
exit 0
171+
fi
172+
fi
173+
echo "Homebrew formula not updated for ${RELEASE_TAG} yet (attempt ${attempt}/18)."
174+
sleep 10
175+
done
176+
echo "Homebrew formula did not update for ${RELEASE_TAG}; failing release gate."
177+
exit 1

.github/workflows/winget.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Publish WinGet Manifest
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
release_tag:
7+
description: "Stable release tag to publish (e.g. v0.1.0)"
8+
required: true
9+
type: string
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
winget:
16+
name: Update winget-pkgs
17+
runs-on: windows-latest
18+
env:
19+
WINGET_PAT: ${{ secrets.WINGET_PAT }}
20+
steps:
21+
- name: Resolve target tag
22+
id: meta
23+
shell: pwsh
24+
env:
25+
DISPATCH_RELEASE_TAG: ${{ inputs.release_tag }}
26+
run: |
27+
$tag = $env:DISPATCH_RELEASE_TAG
28+
29+
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
30+
throw "WinGet publishing only supports stable tags (vMAJOR.MINOR.PATCH). Got: $tag"
31+
}
32+
33+
"release_tag=$tag" >> $env:GITHUB_OUTPUT
34+
35+
- name: Require WinGet PAT
36+
shell: pwsh
37+
run: |
38+
if ([string]::IsNullOrWhiteSpace($env:WINGET_PAT)) {
39+
throw "Missing WINGET_PAT secret. Configure it in repo secrets before publishing WinGet manifests."
40+
}
41+
42+
# fork-user defaults to repository_owner; for org-owned repos, that is the org,
43+
# while WINGET_PAT is often a maintainer's personal classic PAT. Komac then tries to
44+
# CreateRef on org/winget-pkgs and fails. Set repo/org Actions variable WINGET_FORK_USER
45+
# to the GitHub login that owns both the microsoft/winget-pkgs fork and the PAT.
46+
- name: Publish to WinGet
47+
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
48+
with:
49+
identifier: git-rain.git-rain
50+
release-tag: ${{ steps.meta.outputs.release_tag }}
51+
installers-regex: '_windows_(amd64|arm64|386)\.zip$'
52+
token: ${{ env.WINGET_PAT }}
53+
fork-user: ${{ vars.WINGET_FORK_USER || github.repository_owner }}

0 commit comments

Comments
 (0)