Skip to content

Commit 8c9b6b2

Browse files
committed
feat: add release automation workflows
- Add release-vote.yml: workflow_dispatch to create vote issue with maintainer checklist and PR changelog - Add release-github.yml: tag-push triggered draft release with categorized notes (Breaking/Features/Fixes/Other) - Update RELEASE_CHECKLIST.md to document new automated flow - PR metadata fallback from commit messages when API lookup fails - First-release edge case handling (no previous tag) Signed-off-by: Akash Singhal <akashsinghal@microsoft.com>
1 parent 6c439ff commit 8c9b6b2

File tree

4 files changed

+323
-9
lines changed

4 files changed

+323
-9
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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-github
15+
16+
on:
17+
push:
18+
tags:
19+
- v*
20+
21+
permissions:
22+
contents: write
23+
24+
jobs:
25+
create-release:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v6
30+
with:
31+
fetch-depth: 0
32+
33+
- name: Get previous release tag
34+
id: prev_tag
35+
run: |
36+
# Get the tag before the current one
37+
PREV_TAG=$(git tag --sort=-v:refname \
38+
| grep -xFv "${GITHUB_REF_NAME}" | head -n 1)
39+
echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
40+
41+
- name: Generate release notes
42+
id: notes
43+
env:
44+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
run: |
46+
TAG_NAME="${GITHUB_REF_NAME}"
47+
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
48+
REPO="${{ github.repository }}"
49+
VERSION="${TAG_NAME#v}"
50+
51+
# Extract PR numbers from commit messages, then look up
52+
# each PR's metadata via the GitHub API. Falls back to
53+
# commit message parsing if the API lookup fails.
54+
if [ -n "$PREV_TAG" ]; then
55+
PR_NUMBERS=$(git log --oneline "$PREV_TAG..$TAG_NAME" \
56+
| grep -oP '#\K\d+' | sort -n -u || true)
57+
else
58+
# First release: include all commits
59+
PR_NUMBERS=$(git log --oneline "$TAG_NAME" \
60+
| grep -oP '#\K\d+' | sort -n -u || true)
61+
fi
62+
63+
# Build a map of PR numbers to commit message data
64+
# for fallback when API lookup fails.
65+
declare -A COMMIT_MAP
66+
while IFS= read -r line; do
67+
if [[ "$line" =~ \#([0-9]+)\) ]]; then
68+
COMMIT_MAP["${BASH_REMATCH[1]}"]="$line"
69+
fi
70+
done < <(if [ -n "$PREV_TAG" ]; then
71+
git log --format="%s%x09%an" "$PREV_TAG..$TAG_NAME"
72+
else
73+
git log --format="%s%x09%an" "$TAG_NAME"
74+
fi)
75+
76+
PRS=""
77+
for num in $PR_NUMBERS; do
78+
PR_DATA=$(gh pr view "$num" --repo "$REPO" \
79+
--json number,title,author \
80+
--jq '"\(.number)\t\(.title)\t\(.author.login)"' \
81+
2>/dev/null || true)
82+
if [ -n "$PR_DATA" ]; then
83+
PRS="${PRS}${PR_DATA}"$'\n'
84+
elif [ -n "${COMMIT_MAP[$num]:-}" ]; then
85+
# Fallback: parse title and author from commit msg.
86+
# Author is plain text (no @) to avoid wrong tags.
87+
RAW="${COMMIT_MAP[$num]}"
88+
TITLE=$(echo "$RAW" \
89+
| sed -E "s/ *\(#${num}\)\t.*$//" )
90+
AUTHOR=$(echo "$RAW" \
91+
| sed -E 's/^.*\t//')
92+
PRS="${PRS}$(printf '%s\t%s\t~%s' \
93+
"$num" "$TITLE" "$AUTHOR")"$'\n'
94+
fi
95+
done
96+
97+
# Categorize PRs by conventional commit prefix
98+
BREAKING=""
99+
FEATURES=""
100+
FIXES=""
101+
OTHER=""
102+
COMMITS=""
103+
104+
while IFS=$'\t' read -r num title author; do
105+
[ -z "$num" ] && continue
106+
# Fallback authors are prefixed with ~ (no @ tag)
107+
if [[ "$author" == ~* ]]; then
108+
entry="* ${title} by ${author#\~} in #${num}"
109+
else
110+
entry="* ${title} by @${author} in #${num}"
111+
fi
112+
113+
case "$title" in
114+
*!:*|*"BREAKING"*)
115+
BREAKING="${BREAKING}${entry}"$'\n'
116+
;;
117+
feat:*|feat\(*\):*)
118+
FEATURES="${FEATURES}${entry}"$'\n'
119+
;;
120+
fix:*|fix\(*\):*)
121+
FIXES="${FIXES}${entry}"$'\n'
122+
;;
123+
*)
124+
OTHER="${OTHER}${entry}"$'\n'
125+
;;
126+
esac
127+
128+
COMMITS="${COMMITS}${entry}"$'\n'
129+
done <<< "$PRS"
130+
131+
# Build release notes
132+
NUGET_URL="https://www.nuget.org/packages/OrasProject.Oras/${VERSION}"
133+
NOTES="The NuGet package is available at [nuget.org](${NUGET_URL})."
134+
NOTES="${NOTES}"$'\n'
135+
136+
if [ -n "$BREAKING" ]; then
137+
NOTES="${NOTES}"$'\n'"## Breaking Changes"$'\n\n'"${BREAKING}"
138+
fi
139+
if [ -n "$FEATURES" ]; then
140+
NOTES="${NOTES}"$'\n'"## New Features"$'\n\n'"${FEATURES}"
141+
fi
142+
if [ -n "$FIXES" ]; then
143+
NOTES="${NOTES}"$'\n'"## Bug Fixes"$'\n\n'"${FIXES}"
144+
fi
145+
if [ -n "$OTHER" ]; then
146+
NOTES="${NOTES}"$'\n'"## Other Changes"$'\n\n'"${OTHER}"
147+
fi
148+
149+
NOTES="${NOTES}"$'\n'"## Detailed Commits"$'\n\n'"${COMMITS}"
150+
if [ -n "$PREV_TAG" ]; then
151+
CHANGELOG="https://github.com/${REPO}/compare/${PREV_TAG}...${TAG_NAME}"
152+
NOTES="${NOTES}"$'\n'"**Full Changelog**: [${PREV_TAG}...${TAG_NAME}](${CHANGELOG})"
153+
fi
154+
155+
# Write notes to file for gh release
156+
echo "$NOTES" > release-notes.md
157+
158+
- name: Create GitHub Release
159+
env:
160+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
161+
run: |
162+
TAG_NAME="${GITHUB_REF_NAME}"
163+
164+
# Determine if this is a pre-release
165+
PRERELEASE_FLAG=""
166+
if echo "$TAG_NAME" | grep -qE '[-](alpha|beta|rc|preview)'; then
167+
PRERELEASE_FLAG="--prerelease"
168+
fi
169+
170+
gh release create "$TAG_NAME" \
171+
--repo "${{ github.repository }}" \
172+
--title "$TAG_NAME" \
173+
--notes-file release-notes.md \
174+
--draft \
175+
$PRERELEASE_FLAG

.github/workflows/release-nuget.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
name: release-nuget
1515

1616
on:
17-
push:
18-
tags:
19-
- v*
17+
release:
18+
types: [published]
2019

2120
jobs:
2221
build:
@@ -30,7 +29,9 @@ jobs:
3029
dotnet-version: '8.0.x'
3130
- name: Extract Version
3231
id: version
33-
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
32+
run: |
33+
TAG_NAME="${{ github.event.release.tag_name }}"
34+
echo "version=${TAG_NAME#v}" >> $GITHUB_OUTPUT
3435
- name: Build nuget package
3536
run: dotnet build ./src/OrasProject.Oras --configuration Release /p:PackageVersion=${{ steps.version.outputs.version }}
3637
- name: Publish nuget package

.github/workflows/release-vote.yml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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-vote
15+
16+
on:
17+
workflow_dispatch:
18+
inputs:
19+
tag_name:
20+
description: >
21+
The tag name for the new release (e.g., v1.0.0).
22+
Must be a valid SemVer2 version prefixed with 'v'.
23+
required: true
24+
type: string
25+
commit_sha:
26+
description: >
27+
The commit SHA to tag for this release.
28+
Defaults to the latest commit on the default branch.
29+
required: false
30+
type: string
31+
32+
permissions:
33+
issues: write
34+
35+
jobs:
36+
create-vote-issue:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Checkout
40+
uses: actions/checkout@v6
41+
with:
42+
fetch-depth: 0
43+
44+
- name: Resolve commit SHA
45+
id: resolve
46+
run: |
47+
if [ -n "${{ inputs.commit_sha }}" ]; then
48+
echo "sha=${{ inputs.commit_sha }}" >> "$GITHUB_OUTPUT"
49+
else
50+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
51+
fi
52+
53+
- name: Get previous release tag
54+
id: prev_tag
55+
run: |
56+
PREV_TAG=$(git tag --sort=-v:refname | head -n 1)
57+
echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
58+
59+
- name: Parse maintainers
60+
id: maintainers
61+
run: |
62+
# Parse MAINTAINERS.md into checkbox list
63+
# Format: "- Name (@handle)" -> "- [ ] Name (@handle)"
64+
{
65+
echo 'list<<EOF'
66+
grep -E '^\s*-\s+.+\(@.+\)' MAINTAINERS.md \
67+
| sed 's/^\s*-\s*/- [ ] /'
68+
echo 'EOF'
69+
} >> "$GITHUB_OUTPUT"
70+
71+
- name: Generate PR list
72+
id: changelog
73+
env:
74+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
run: |
76+
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
77+
COMMIT_SHA="${{ steps.resolve.outputs.sha }}"
78+
79+
if [ -n "$PREV_TAG" ]; then
80+
PR_NUMBERS=$(git log --oneline \
81+
"$PREV_TAG..$COMMIT_SHA" \
82+
| grep -oP '#\K\d+' | sort -n -u || true)
83+
else
84+
# First release: include all commits
85+
PR_NUMBERS=$(git log --oneline "$COMMIT_SHA" \
86+
| grep -oP '#\K\d+' | sort -n -u || true)
87+
fi
88+
89+
# Format as markdown list with linked PR references
90+
{
91+
echo 'pr_list<<EOF'
92+
echo "$PR_NUMBERS" | while read -r num; do
93+
if [ -n "$num" ]; then
94+
echo "- #${num}"
95+
fi
96+
done
97+
echo 'EOF'
98+
} >> "$GITHUB_OUTPUT"
99+
100+
- name: Create vote issue
101+
env:
102+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103+
run: |
104+
TAG_NAME="${{ inputs.tag_name }}"
105+
COMMIT_SHA="${{ steps.resolve.outputs.sha }}"
106+
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
107+
MAINTAINERS="${{ steps.maintainers.outputs.list }}"
108+
PR_LIST="${{ steps.changelog.outputs.pr_list }}"
109+
REPO="${{ github.repository }}"
110+
COUNT=$(echo "$MAINTAINERS" | wc -l)
111+
112+
# Build changelog context
113+
if [ -n "$PREV_TAG" ]; then
114+
CHANGES_HEADER="The changes compared to \`${PREV_TAG}\` include:"
115+
CHANGELOG_LINK="See [the full changelog](https://github.com/${REPO}/compare/${PREV_TAG}...${COMMIT_SHA}) for more details."
116+
else
117+
CHANGES_HEADER="The changes in this release include:"
118+
CHANGELOG_LINK=""
119+
fi
120+
121+
BODY="At least 2 approvals are needed from the ${COUNT} maintainers for tagging ${COMMIT_SHA} as \`${TAG_NAME}\`."
122+
BODY="${BODY}"$'\n\n'"${MAINTAINERS}"
123+
BODY="${BODY}"$'\n\n'"${CHANGES_HEADER}"
124+
BODY="${BODY}"$'\n\n'"${PR_LIST}"
125+
if [ -n "$CHANGELOG_LINK" ]; then
126+
BODY="${BODY}"$'\n\n'"${CHANGELOG_LINK}"
127+
fi
128+
BODY="${BODY}"$'\n'"Please respond LGTM or REJECT (with reasoning)."
129+
130+
gh issue create \
131+
--repo "$REPO" \
132+
--title "Vote for release of \`${TAG_NAME}\`" \
133+
--body "$BODY"

RELEASE_CHECKLIST.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ This document describes the checklist to publish a release through the [GitHub r
77
## Release Process
88

99
1. Determine a [SemVer2](https://semver.org/)-valid version prefixed with the letter `v` for release. For example, `v1.0.0-rc.1`.
10-
2. Create an issue to vote for a new release (see [example](https://github.com/oras-project/oras-dotnet/issues/103)).
11-
3. After the vote passes, [draft a release](https://github.com/oras-project/oras-dotnet/releases/new) using the determined version as the tag name, targeting the voted commit. A tag will be automatically created from the target commit when the release is published. If the voted commit cannot be found in the _recent commits_, a release branch (e.g. `release-1.0`) is required to be created from the voted commit and used as the target branch.
12-
4. Compose and revise the release note and optionally select `Set as a pre-release` depending on the version.
13-
5. Publish the release on GitHub.
14-
6. A [workflow](https://github.com/oras-project/oras-dotnet/actions/workflows/release-nuget.yml) will be triggered automatically by tag creation mentioned in the step 3 for publishing the release to NuGet.
10+
2. Run the [`release-vote`](.github/workflows/release-vote.yml) workflow from the Actions tab. Provide the tag name and optionally a commit SHA (defaults to latest on `main`). This creates a vote issue listing maintainers and the changelog since the last release (see [example](https://github.com/oras-project/oras-dotnet/issues/103)).
11+
3. After the vote passes, push the tag targeting the voted commit:
12+
```bash
13+
git tag <tag_name> <commit_sha>
14+
git push origin <tag_name>
15+
```
16+
If the voted commit cannot be found in the _recent commits_, a release branch (e.g. `release-1.0`) is required to be created from the voted commit and used as the target branch.
17+
4. The tag push automatically triggers [`release-github`](.github/workflows/release-github.yml), which creates a **draft** GitHub Release with auto-generated release notes. Pre-release versions (alpha, beta, rc, preview) are automatically marked as pre-releases.
18+
5. Review the draft release on the [Releases page](https://github.com/oras-project/oras-dotnet/releases). Edit the release notes if needed.
19+
6. Publish the release. This triggers [`release-nuget`](.github/workflows/release-nuget.yml), which builds and publishes the NuGet package.
1520
7. Wait for NuGet to validate the newly released package.
1621
8. Announce the release in the community.
1722

0 commit comments

Comments
 (0)