-
Notifications
You must be signed in to change notification settings - Fork 20
feat: add release automation workflows #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
635125f
78333dd
73a0a81
7224bad
ed43420
caefc3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| # Copyright The ORAS Authors. | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| name: release-github | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - v* | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
| create-release: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Validate tag name | ||
| run: | | ||
| if ! echo "${GITHUB_REF_NAME}" \ | ||
| | grep -qP '^v\d+\.\d+\.\d+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$'; then | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the SemVer validation regex is duplicated across all three workflows. Consider extracting to a reusable workflow or composite action to avoid drift. |
||
| echo "::error::Invalid tag '${GITHUB_REF_NAME}'." \ | ||
| "Must be valid SemVer2: vMAJOR.MINOR.PATCH[-prerelease][+metadata]." | ||
akashsinghal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Get previous release tag | ||
| id: prev_tag | ||
| run: | | ||
| # Get the most recent tag reachable from this tag, | ||
| # excluding the current tag itself. | ||
| PREV_TAG=$(git tag --merged "${GITHUB_REF_NAME}" \ | ||
| --sort=-v:refname \ | ||
| | grep -xFv "${GITHUB_REF_NAME}" | head -n 1) | ||
| echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Generate release notes | ||
| id: notes | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| TAG_NAME="${GITHUB_REF_NAME}" | ||
| PREV_TAG="${{ steps.prev_tag.outputs.tag }}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same concern — an existing tag containing shell metacharacters could be picked up by |
||
| REPO="${{ github.repository }}" | ||
| VERSION="${TAG_NAME#v}" | ||
|
|
||
| # Extract PR numbers from commit messages, then look up | ||
| # each PR's metadata via the GitHub API. Falls back to | ||
| # commit message parsing if the API lookup fails. | ||
| if [ -n "$PREV_TAG" ]; then | ||
| PR_NUMBERS=$(git log --oneline "$PREV_TAG..$TAG_NAME" \ | ||
| | grep -oP '#\K\d+' | sort -n -u || true) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| else | ||
| # First release: include all commits | ||
| PR_NUMBERS=$(git log --oneline "$TAG_NAME" \ | ||
| | grep -oP '#\K\d+' | sort -n -u || true) | ||
| fi | ||
|
|
||
| # Build a map of PR numbers to commit message data | ||
| # for fallback when API lookup fails. | ||
| declare -A COMMIT_MAP | ||
| while IFS= read -r line; do | ||
| if [[ "$line" =~ \#([0-9]+)\) ]]; then | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| COMMIT_MAP["${BASH_REMATCH[1]}"]="$line" | ||
| fi | ||
| done < <(if [ -n "$PREV_TAG" ]; then | ||
| git log --format="%s%x09%an" "$PREV_TAG..$TAG_NAME" | ||
| else | ||
| git log --format="%s%x09%an" "$TAG_NAME" | ||
| fi) | ||
|
|
||
| PRS="" | ||
| for num in $PR_NUMBERS; do | ||
| PR_DATA=$(gh pr view "$num" --repo "$REPO" \ | ||
| --json number,title,author \ | ||
| --jq '"\(.number)\t\(.title)\t\(.author.login)"' \ | ||
| 2>/dev/null || true) | ||
| if [ -n "$PR_DATA" ]; then | ||
| PRS="${PRS}${PR_DATA}"$'\n' | ||
| elif [ -n "${COMMIT_MAP[$num]:-}" ]; then | ||
| # Fallback: parse title and author from commit msg. | ||
| # Author is plain text (no @) to avoid wrong tags. | ||
| RAW="${COMMIT_MAP[$num]}" | ||
| TITLE=$(echo "$RAW" \ | ||
| | sed -E "s/ *\(#${num}\)\t.*$//" ) | ||
| AUTHOR=$(echo "$RAW" \ | ||
| | sed -E 's/^.*\t//') | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| PRS="${PRS}$(printf '%s\t%s\t~%s' \ | ||
| "$num" "$TITLE" "$AUTHOR")"$'\n' | ||
| fi | ||
| done | ||
|
|
||
| # Categorize PRs by conventional commit prefix | ||
| BREAKING="" | ||
| FEATURES="" | ||
| FIXES="" | ||
| OTHER="" | ||
| COMMITS="" | ||
|
|
||
| while IFS=$'\t' read -r num title author; do | ||
| [ -z "$num" ] && continue | ||
| # Fallback authors are prefixed with ~ (no @ tag) | ||
| if [[ "$author" == ~* ]]; then | ||
| entry="* ${title} by ${author#~} in #${num}" | ||
| else | ||
| entry="* ${title} by @${author} in #${num}" | ||
| fi | ||
|
|
||
| case "$title" in | ||
| *!:*|*"BREAKING CHANGE"*) | ||
| BREAKING="${BREAKING}${entry}"$'\n' | ||
| ;; | ||
| feat:*|feat\(*\):*) | ||
| FEATURES="${FEATURES}${entry}"$'\n' | ||
| ;; | ||
| fix:*|fix\(*\):*) | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| FIXES="${FIXES}${entry}"$'\n' | ||
| ;; | ||
| *) | ||
| OTHER="${OTHER}${entry}"$'\n' | ||
| ;; | ||
| esac | ||
|
|
||
| COMMITS="${COMMITS}${entry}"$'\n' | ||
| done <<< "$PRS" | ||
|
|
||
| # If no PRs found, list raw commits as fallback | ||
| if [ -z "$COMMITS" ]; then | ||
| if [ -n "$PREV_TAG" ]; then | ||
| RAW_LOG=$(git log --format="* %s (%an)" \ | ||
| "$PREV_TAG..$TAG_NAME") | ||
| else | ||
| RAW_LOG=$(git log --format="* %s (%an)" \ | ||
| "$TAG_NAME") | ||
| fi | ||
| COMMITS="${RAW_LOG}"$'\n' | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fi | ||
|
|
||
| # Build release notes | ||
| NUGET_URL="https://www.nuget.org/packages/OrasProject.Oras/${VERSION}" | ||
| NOTES="The NuGet package is available at [nuget.org](${NUGET_URL})." | ||
| NOTES="${NOTES}"$'\n' | ||
|
|
||
| if [ -n "$BREAKING" ]; then | ||
| NOTES="${NOTES}"$'\n'"## Breaking Changes"$'\n\n'"${BREAKING}" | ||
| fi | ||
| if [ -n "$FEATURES" ]; then | ||
| NOTES="${NOTES}"$'\n'"## New Features"$'\n\n'"${FEATURES}" | ||
| fi | ||
| if [ -n "$FIXES" ]; then | ||
| NOTES="${NOTES}"$'\n'"## Bug Fixes"$'\n\n'"${FIXES}" | ||
| fi | ||
| if [ -n "$OTHER" ]; then | ||
| NOTES="${NOTES}"$'\n'"## Other Changes"$'\n\n'"${OTHER}" | ||
| fi | ||
|
|
||
| NOTES="${NOTES}"$'\n'"## Detailed Commits"$'\n\n'"${COMMITS}" | ||
| if [ -n "$PREV_TAG" ]; then | ||
| CHANGELOG="https://github.com/${REPO}/compare/${PREV_TAG}...${TAG_NAME}" | ||
| NOTES="${NOTES}"$'\n'"**Full Changelog**: [${PREV_TAG}...${TAG_NAME}](${CHANGELOG})" | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fi | ||
|
|
||
| # Write notes to file for gh release | ||
| echo "$NOTES" > release-notes.md | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| - name: Create GitHub Release | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| TAG_NAME="${GITHUB_REF_NAME}" | ||
|
|
||
| # Determine if this is a pre-release | ||
| PRERELEASE_FLAG="" | ||
| if echo "$TAG_NAME" | grep -qE '[-](alpha|beta|rc|preview)'; then | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we check for the presence of a hyphen after the patch version instead? Per SemVer2, any version with a pre-release identifier (i.e. a |
||
| PRERELEASE_FLAG="--prerelease" | ||
| fi | ||
|
|
||
| gh release create "$TAG_NAME" \ | ||
| --repo "${{ github.repository }}" \ | ||
| --title "$TAG_NAME" \ | ||
| --notes-file release-notes.md \ | ||
| --draft \ | ||
| $PRERELEASE_FLAG | ||
akashsinghal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,9 +14,11 @@ | |
| name: release-nuget | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - v* | ||
| release: | ||
| types: [published] | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| build: | ||
|
|
@@ -30,8 +32,25 @@ jobs: | |
| dotnet-version: '8.0.x' | ||
| - name: Extract Version | ||
| id: version | ||
| run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT | ||
| run: | | ||
| TAG_NAME="${{ github.event.release.tag_name }}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here for |
||
| if ! echo "$TAG_NAME" \ | ||
| | grep -qP '^v\d+\.\d+\.\d+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$'; then | ||
| echo "::error::Invalid tag '${TAG_NAME}'." \ | ||
| "Must be valid SemVer2: vMAJOR.MINOR.PATCH[-prerelease][+metadata]." | ||
akashsinghal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| exit 1 | ||
| fi | ||
| echo "version=${TAG_NAME#v}" >> $GITHUB_OUTPUT | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - name: Build nuget package | ||
| run: dotnet build ./src/OrasProject.Oras --configuration Release /p:PackageVersion=${{ steps.version.outputs.version }} | ||
| - name: Upload nupkg and checksum to release | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| NUPKG: ./src/OrasProject.Oras/bin/Release/OrasProject.Oras.${{ steps.version.outputs.version }}.nupkg | ||
| run: | | ||
| sha256sum "$NUPKG" | awk '{print $1}' > "${NUPKG}.sha256" | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| gh release upload "${{ github.event.release.tag_name }}" \ | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "$NUPKG" \ | ||
| "${NUPKG}.sha256" | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - name: Publish nuget package | ||
| run: dotnet nuget push ./src/OrasProject.Oras/bin/Release/OrasProject.Oras.${{ steps.version.outputs.version }}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} | ||
| run: dotnet nuget push ./src/OrasProject.Oras/bin/Release/OrasProject.Oras.${{ steps.version.outputs.version }}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # Copyright The ORAS Authors. | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| name: release-vote | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag_name: | ||
| description: > | ||
| The tag name for the new release (e.g., v1.0.0). | ||
| Must be a valid SemVer2 version prefixed with 'v'. | ||
| required: true | ||
| type: string | ||
| commit_sha: | ||
| description: > | ||
| The commit SHA to tag for this release. | ||
| Defaults to the latest commit on the default branch. | ||
| required: false | ||
| type: string | ||
|
|
||
| permissions: | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| contents: read | ||
| issues: write | ||
|
|
||
| jobs: | ||
| create-vote-issue: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fetch-depth: 0 | ||
|
|
||
| - name: Validate tag name | ||
| run: | | ||
| TAG_NAME="${{ inputs.tag_name }}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We should pass all user-controlled inputs via - name: Validate tag name
env:
TAG_NAME: ${{ inputs.tag_name }}
run: |
if ! echo "$TAG_NAME" ...Same pattern applies to all |
||
| if ! echo "$TAG_NAME" \ | ||
| | grep -qP '^v\d+\.\d+\.\d+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$'; then | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| echo "::error::Invalid tag name '${TAG_NAME}'." \ | ||
| "Must be valid SemVer2: vMAJOR.MINOR.PATCH[-prerelease][+metadata]." | ||
akashsinghal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Resolve commit SHA | ||
| id: resolve | ||
| run: | | ||
| if [ -n "${{ inputs.commit_sha }}" ]; then | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here for |
||
| SHA="${{ inputs.commit_sha }}" | ||
| if ! git rev-parse --verify "${SHA}^{commit}" >/dev/null 2>&1; then | ||
| echo "::error::Invalid commit SHA: ${SHA}" | ||
| exit 1 | ||
| fi | ||
| echo "sha=${SHA}" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | ||
akashsinghal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fi | ||
|
|
||
| - name: Get previous release tag | ||
| id: prev_tag | ||
| run: | | ||
| COMMIT_SHA="${{ steps.resolve.outputs.sha }}" | ||
| PREV_TAG=$(git tag --merged "$COMMIT_SHA" \ | ||
| --sort=-v:refname | head -n 1) | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Parse maintainers | ||
| id: maintainers | ||
| run: | | ||
| # Parse MAINTAINERS.md into checkbox list | ||
| # Format: "- Name (@handle)" -> "- [ ] Name (@handle)" | ||
| { | ||
| echo 'list<<EOF' | ||
| grep -E '^\s*-\s+.+\(@.+\)' MAINTAINERS.md \ | ||
| | sed 's/^\s*-\s*/- [ ] /' | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| echo 'EOF' | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Generate PR list | ||
| id: changelog | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
akashsinghal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| run: | | ||
| PREV_TAG="${{ steps.prev_tag.outputs.tag }}" | ||
| COMMIT_SHA="${{ steps.resolve.outputs.sha }}" | ||
|
|
||
| if [ -n "$PREV_TAG" ]; then | ||
| PR_NUMBERS=$(git log --oneline \ | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "$PREV_TAG..$COMMIT_SHA" \ | ||
| | grep -oP '#\K\d+' | sort -n -u || true) | ||
| else | ||
| # First release: include all commits | ||
| PR_NUMBERS=$(git log --oneline "$COMMIT_SHA" \ | ||
| | grep -oP '#\K\d+' | sort -n -u || true) | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fi | ||
|
|
||
| # Format as markdown list with linked PR references. | ||
| { | ||
| echo 'pr_list<<EOF' | ||
| if [ -z "$PR_NUMBERS" ]; then | ||
| echo "_No PRs found — direct commits only._" | ||
| else | ||
| echo "$PR_NUMBERS" | while read -r num; do | ||
| if [ -n "$num" ]; then | ||
| echo "- #${num}" | ||
| fi | ||
| done | ||
| fi | ||
| echo 'EOF' | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Create vote issue | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| TAG_NAME="${{ inputs.tag_name }}" | ||
| COMMIT_SHA="${{ steps.resolve.outputs.sha }}" | ||
| PREV_TAG="${{ steps.prev_tag.outputs.tag }}" | ||
| MAINTAINERS="${{ steps.maintainers.outputs.list }}" | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue. Multiline step outputs ( |
||
| PR_LIST="${{ steps.changelog.outputs.pr_list }}" | ||
| REPO="${{ github.repository }}" | ||
| COUNT=$(echo "$MAINTAINERS" | grep -c '\- \[ \]') | ||
|
|
||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # Build changelog context | ||
| if [ -n "$PREV_TAG" ]; then | ||
| CHANGES_HEADER="The changes compared to \`${PREV_TAG}\` include:" | ||
| CHANGELOG_LINK="See [the full changelog](https://github.com/${REPO}/compare/${PREV_TAG}...${COMMIT_SHA}) for more details." | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else | ||
| CHANGES_HEADER="The changes in this release include:" | ||
| CHANGELOG_LINK="" | ||
| fi | ||
|
|
||
| BODY="At least 2 approvals are needed from the ${COUNT} maintainers for tagging ${COMMIT_SHA} as \`${TAG_NAME}\`." | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| BODY="${BODY}"$'\n\n'"${MAINTAINERS}" | ||
| BODY="${BODY}"$'\n\n'"${CHANGES_HEADER}" | ||
| BODY="${BODY}"$'\n\n'"${PR_LIST}" | ||
| if [ -n "$CHANGELOG_LINK" ]; then | ||
| BODY="${BODY}"$'\n\n'"${CHANGELOG_LINK}" | ||
| fi | ||
| BODY="${BODY}"$'\n'"Please respond LGTM or REJECT (with reasoning)." | ||
|
|
||
| gh issue create \ | ||
| --repo "$REPO" \ | ||
| --title "Vote for release of \`${TAG_NAME}\`" \ | ||
| --body "$BODY" | ||
akashsinghal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.