Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions .github/workflows/release-github.yml
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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]."
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 }}"
Copy link
Contributor

Choose a reason for hiding this comment

The 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 git tag --merged and injected here.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grep -oP '#\K\d+' matches any #number in commit messages, not just PR references. Issue references like "Fixes #42" or comments like "addresses #100" would be picked up as false positives. Should we match the conventional merge commit format (#<number>) instead?

grep -oP '\(#\K\d+(?=\))'

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
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//')
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\(*\):*)
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'
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})"
fi

# Write notes to file for gh release
echo "$NOTES" > release-notes.md

- 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 - suffix after MAJOR.MINOR.PATCH) is a pre-release. The current pattern misses identifiers like dev, nightly, etc.

PRERELEASE_FLAG="--prerelease"
fi

gh release create "$TAG_NAME" \
--repo "${{ github.repository }}" \
--title "$TAG_NAME" \
--notes-file release-notes.md \
--draft \
$PRERELEASE_FLAG
29 changes: 24 additions & 5 deletions .github/workflows/release-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
name: release-nuget

on:
push:
tags:
- v*
release:
types: [published]

permissions:
contents: write

jobs:
build:
Expand All @@ -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 }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here for ${{ github.event.release.tag_name }}.

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]."
exit 1
fi
echo "version=${TAG_NAME#v}" >> $GITHUB_OUTPUT
- 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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: awk '{print $1}' strips the filename from the checksum. The standard sha256sum --check format expects <hash> <filename>. Should we keep the full output so users can verify with sha256sum --check *.sha256?

gh release upload "${{ github.event.release.tag_name }}" \
"$NUPKG" \
"${NUPKG}.sha256"
- 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
155 changes: 155 additions & 0 deletions .github/workflows/release-vote.yml
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:
contents: read
issues: write

jobs:
create-vote-issue:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Validate tag name
run: |
TAG_NAME="${{ inputs.tag_name }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

${{ inputs.tag_name }} is directly interpolated into the shell by GitHub's template engine before the script runs. This is vulnerable to script injection — a crafted input can execute arbitrary commands.

We should pass all user-controlled inputs via env: mapping:

      - name: Validate tag name
        env:
          TAG_NAME: ${{ inputs.tag_name }}
        run: |
          if ! echo "$TAG_NAME" ...

Same pattern applies to all ${{ inputs.* }} and ${{ steps.*.outputs.* }} expressions used directly in run: blocks across all three workflow files.

if ! echo "$TAG_NAME" \
| grep -qP '^v\d+\.\d+\.\d+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$'; then
echo "::error::Invalid tag name '${TAG_NAME}'." \
"Must be valid SemVer2: vMAJOR.MINOR.PATCH[-prerelease][+metadata]."
exit 1
fi

- name: Resolve commit SHA
id: resolve
run: |
if [ -n "${{ inputs.commit_sha }}" ]; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here for ${{ inputs.commit_sha }}.

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"
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)
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*/- [ ] /'
echo 'EOF'
} >> "$GITHUB_OUTPUT"

- name: Generate PR list
id: changelog
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
COMMIT_SHA="${{ steps.resolve.outputs.sha }}"

if [ -n "$PREV_TAG" ]; then
PR_NUMBERS=$(git log --oneline \
"$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)
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 }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue. Multiline step outputs (maintainers.outputs.list, changelog.outputs.pr_list) are particularly dangerous — embedded newlines or shell metacharacters in maintainer names or PR titles can break out of the double-quoted string.

PR_LIST="${{ steps.changelog.outputs.pr_list }}"
REPO="${{ github.repository }}"
COUNT=$(echo "$MAINTAINERS" | grep -c '\- \[ \]')

# 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."
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}\`."
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"
Loading
Loading