Skip to content

Commit d593d50

Browse files
TerryHoweclaude
andauthored
feat: add gitops release workflow with goreleaser (#1161)
## Summary Replaces the manual issue-vote → tag → release process with a PR-based GitOps workflow. ## Changes - **`.goreleaser.yaml`** — Minimal config for a Go library (no binary builds, archives, or checksums). `prerelease: auto` marks tags containing `-alpha`, `-beta`, or `-rc` as pre-release on GitHub. - **`.github/workflows/release.yml`** — Triggers on merge of a `release/vX.Y.Z` branch into `v2`. Extracts the version from the branch name, tags the merge commit, and publishes the GitHub Release using the PR body as release notes. The `goreleaser/goreleaser-action` is pinned to `e435ccd` (v6.4.0). - **`RELEASES.md`** — Documents the full release process, the `--allow-empty` branch convention, and three levels of local testing with `act`. - **`.github/act/release-event.json`** — Mock PR event payload for local workflow testing. ## How a release works 1. Create a branch named `release/vX.Y.Z` from `v2` with an empty commit (required for GitHub to allow a PR with no code diff): ```bash git fetch upstream git checkout -b release/v2.7.0 upstream/v2 git commit --allow-empty -s -m "chore: prepare release v2.7.0" git push origin release/v2.7.0 ``` 2. Open a PR to `v2` — write the release notes in the PR description (same format as today's release issues) 3. Get the required approvals via branch protection (replaces the issue vote) 4. Merge — the workflow automatically tags the commit and publishes the GitHub Release using the PR body as release notes The release PR does not need to contain the changes being released — those are already on `v2`. The PR is purely a trigger. ## Local testing Three levels of validation are available without triggering a real release: ```bash # 1. Validate goreleaser config goreleaser check # 2. Validate workflow structure and job matching (dry run) act pull_request -e .github/act/release-event.json -W .github/workflows/release.yml -n # 3. Full end-to-end run (Colima + cached actions required) act pull_request \ -e .github/act/release-event.json \ -W .github/workflows/release.yml \ -s GITHUB_TOKEN=fake \ --pull=false \ --action-offline-mode \ --container-daemon-socket - ``` The end-to-end run executes through checkout → Go setup → version extraction (printing `version=vX.Y.Z`) → then fails at `git push` with a permission error. That is the expected safe stopping point — no tag is pushed and no release is created. ## Notes - Branch protection on `v2` should require the same approval quorum currently used in release issues (3 of 4 owners) - Pre-release tags (`-alpha`, `-beta`, `-rc`) are automatically marked as pre-release on GitHub 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Terry Howe <terrylhowe@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5fd67f9 commit d593d50

4 files changed

Lines changed: 232 additions & 0 deletions

File tree

.github/act/release-event.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"action": "closed",
3+
"pull_request": {
4+
"merged": true,
5+
"number": 999,
6+
"title": "chore: prepare release v2.7.0",
7+
"body": "## New Features\n\n* Test feature\n\n## Bug Fixes\n\n* Test fix",
8+
"head": {
9+
"ref": "release/v2.7.0",
10+
"sha": "abc123def456"
11+
},
12+
"base": {
13+
"ref": "v2"
14+
}
15+
},
16+
"repository": {
17+
"full_name": "oras-project/oras-go",
18+
"name": "oras-go",
19+
"owner": {
20+
"login": "oras-project"
21+
}
22+
}
23+
}

.github/workflows/release.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright The ORAS Authors.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
name: release
15+
16+
on:
17+
pull_request:
18+
types: [closed]
19+
branches:
20+
- v2
21+
22+
jobs:
23+
release:
24+
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') && github.event.pull_request.head.repo.full_name == github.repository
25+
runs-on: ubuntu-latest
26+
permissions:
27+
contents: write
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
31+
with:
32+
fetch-depth: 0
33+
ref: ${{ github.event.pull_request.merge_commit_sha }}
34+
35+
- name: Set up Go
36+
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
37+
with:
38+
go-version-file: go.mod
39+
check-latest: true
40+
41+
- name: Extract version from branch name
42+
id: version
43+
run: |
44+
BRANCH="${{ github.head_ref }}"
45+
VERSION="${BRANCH#release/}"
46+
echo "version=$VERSION" >> $GITHUB_OUTPUT
47+
48+
- name: Validate semantic version
49+
run: |
50+
VERSION="${{ steps.version.outputs.version }}"
51+
if ! [[ "$VERSION" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(alpha|beta|rc)\.[0-9]+)?$ ]]; then
52+
echo "Error: '$VERSION' does not follow semantic versioning"
53+
echo "Expected: vX.Y.Z or vX.Y.Z-(alpha|beta|rc).N"
54+
exit 1
55+
fi
56+
57+
- name: Create and push tag
58+
run: |
59+
git config user.name "github-actions[bot]"
60+
git config user.email "github-actions[bot]@users.noreply.github.com"
61+
git tag ${{ steps.version.outputs.version }} ${{ github.event.pull_request.merge_commit_sha }}
62+
git push origin ${{ steps.version.outputs.version }}
63+
64+
- name: Write release notes from PR body
65+
env:
66+
NOTES: ${{ github.event.pull_request.body }}
67+
run: printf '%s\n' "$NOTES" > /tmp/release-notes.md
68+
69+
- name: Run GoReleaser
70+
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
71+
with:
72+
version: latest
73+
args: release --clean --release-notes /tmp/release-notes.md
74+
env:
75+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.goreleaser.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright The ORAS Authors.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
version: 2
15+
16+
# oras-go is a library — no binary builds or archives needed.
17+
builds:
18+
- skip: true
19+
20+
checksum:
21+
disable: true
22+
23+
release:
24+
# Tags containing -alpha, -beta, or -rc are automatically marked pre-release.
25+
prerelease: auto
26+
draft: false

RELEASES.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Releasing oras-go
2+
3+
Releases are created via a GitOps workflow. Merging a `release/vX.Y.Z` branch
4+
into `v2` automatically tags the commit and publishes the GitHub Release.
5+
6+
## Steps
7+
8+
### 1. Create a release branch
9+
10+
The release branch needs at least one commit so GitHub will allow a PR to be
11+
opened. Use an empty commit as a lightweight marker:
12+
13+
```bash
14+
git fetch upstream
15+
git checkout -b release/v2.7.0 upstream/v2
16+
git commit --allow-empty -s -m "chore: prepare release v2.7.0"
17+
git push origin release/v2.7.0
18+
```
19+
20+
The release does not need to contain the changes being released — those are
21+
already on `v2`. The PR is a trigger: when it merges, the workflow tags the
22+
PR's `merge_commit_sha` (the exact commit that landed on `v2`), which includes
23+
all prior work on the branch.
24+
25+
### 2. Open a pull request
26+
27+
Open a PR from `release/v2.7.0` targeting the `v2` branch. Write the release
28+
notes directly in the PR description using the format from prior releases:
29+
30+
```markdown
31+
## New Features
32+
...
33+
34+
## Bug Fixes
35+
...
36+
37+
## Documentation
38+
...
39+
40+
## Other Changes
41+
...
42+
```
43+
44+
The PR description becomes the GitHub Release body verbatim, so write it in
45+
its final form.
46+
47+
### 3. Get approvals
48+
49+
Branch protection on `v2` requires approval from at least 3 of the 4 owners
50+
listed in [OWNERS.md](OWNERS.md). Reviewers should verify:
51+
52+
- The target commit is correct
53+
- The release notes are accurate and complete
54+
- All CI checks pass
55+
56+
### 4. Merge
57+
58+
Merge the PR. The [release workflow](.github/workflows/release.yml)
59+
automatically:
60+
61+
1. Extracts the version from the branch name (`release/v2.7.0``v2.7.0`)
62+
2. Creates and pushes the git tag
63+
3. Publishes the GitHub Release with the PR body as release notes
64+
65+
## Pre-releases
66+
67+
Tags containing `-alpha`, `-beta`, or `-rc` (e.g., `v2.7.0-rc.1`) are
68+
automatically marked as pre-release on GitHub. Use the same branch naming
69+
convention: `release/v2.7.0-rc.1`.
70+
71+
## Testing the workflow locally
72+
73+
Three levels of local validation are available without triggering a real release:
74+
75+
**1. Validate the goreleaser config:**
76+
```bash
77+
goreleaser check
78+
```
79+
80+
**2. Validate workflow structure and job matching (dry run):**
81+
```bash
82+
act pull_request \
83+
-e .github/act/release-event.json \
84+
-W .github/workflows/release.yml \
85+
-n
86+
```
87+
88+
**3. Run the workflow end-to-end with a fake token (Colima + cached actions required):**
89+
```bash
90+
act pull_request \
91+
-e .github/act/release-event.json \
92+
-W .github/workflows/release.yml \
93+
-s GITHUB_TOKEN=fake \
94+
--pull=false \
95+
--action-offline-mode \
96+
--container-daemon-socket -
97+
```
98+
99+
This runs all steps up to and including version extraction (`version=vX.Y.Z` will
100+
appear in the output). The `git push` step then fails with a permission error —
101+
that is expected and confirms no tag was pushed. The mock event payload is at
102+
`.github/act/release-event.json`.
103+
104+
## Updating the documentation site
105+
106+
After a release, update [oras-www](https://github.com/oras-project/oras-www)
107+
to reflect the new version. See the `CLAUDE.md` in that repository for the
108+
exact steps.

0 commit comments

Comments
 (0)