Skip to content

Release GitHub Tasks #28

Release GitHub Tasks

Release GitHub Tasks #28

# Release GitHub Tasks Workflow
#
# This workflow handles GitHub-specific release tasks:
# 1. Creates a Git tag for the release
# 2. Creates a GitHub Release with release notes
# 3. Creates a PR to merge release branch back to main
# 4. Creates a PR to update PackageValidationBaselineVersion
#
# Designed for idempotency - safe to re-run after partial failures.
#
# Invocation paths:
# - Manual: triggered by a release manager via the Actions UI. The `authorize`
# job gates on admin/maintain permission for the dispatching user.
# - Chained: dispatched automatically by the AzDO release-publish-nuget
# pipeline running as the aspire-repo-bot GitHub App. The authorize job
# recognizes this actor and bypasses the human permission check because
# the AzDO pipeline itself gates on AzDO release-publish permissions.
#
# For full documentation, see: docs/release-process.md
name: Release GitHub Tasks
on:
workflow_dispatch:
inputs:
release_version:
description: 'Release version (e.g., 13.2.0)'
required: true
type: string
commit_sha:
description: 'Commit SHA to tag (from the signed build)'
required: true
type: string
release_branch:
description: 'Release branch name (e.g., release/9.2)'
required: true
type: string
is_prerelease:
description: 'Is this a preview/prerelease?'
required: false
type: boolean
default: false
dry_run:
description: 'Dry run mode - validate and log actions without making changes'
required: false
type: boolean
default: false
# Idempotency flags for re-running after partial failures
skip_tagging:
description: 'Skip tag creation (set true if already completed)'
required: false
type: boolean
default: false
skip_github_release:
description: 'Skip GitHub Release creation (set true if already completed)'
required: false
type: boolean
default: false
skip_merge_pr:
description: 'Skip merge-back PR creation (set true if already completed)'
required: false
type: boolean
default: false
skip_baseline_pr:
description: 'Skip baseline version PR creation (set true if already completed)'
required: false
type: boolean
default: false
# Limit to one release at a time
concurrency:
group: release-github-tasks
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
jobs:
# Three-job layout:
# - `release` does the linear, must-be-sequential work (authorize, tag,
# GitHub release). Splitting these would buy nothing because they
# strictly depend on each other.
# - `merge-pr` and `baseline-pr` run in parallel after `release`. They are
# independent (a failure in one must NOT block the other — that was the
# pre-consolidation behavior we want to preserve), so they each pay
# their own runner-provision tax but run concurrently.
# - `summary` joins on all three with `if: always()` and prints the final
# status.
#
# Each step preserves the previous per-job `if:` gating against the
# `skip_*` inputs so partial-failure re-runs still behave the same way.
release:
name: Release Tasks
runs-on: ubuntu-latest
steps:
# --- Authorize ---------------------------------------------------------
- name: Authorize
env:
GH_TOKEN: ${{ github.token }}
run: |
ACTOR="${{ github.actor }}"
ACTOR_ID="${{ github.actor_id }}"
echo "Checking if $ACTOR (id=$ACTOR_ID) is authorized to run releases..."
# The AzDO release pipeline dispatches this workflow as the
# aspire-repo-bot GitHub App. That path is already gated on AzDO
# release-publish permissions, so we skip the human admin/maintain
# check for the bot after verifying the actor identity.
if [[ "$ACTOR" == "aspire-repo-bot[bot]" ]]; then
ACTOR_INFO=$(gh api "users/$ACTOR" 2>/dev/null || echo '')
ACTOR_TYPE=$(echo "$ACTOR_INFO" | jq -r '.type // empty')
ACTOR_API_ID=$(echo "$ACTOR_INFO" | jq -r '.id // empty')
if [[ "$ACTOR_TYPE" != "Bot" ]]; then
echo "❌ ERROR: $ACTOR resolves to account type '$ACTOR_TYPE', expected 'Bot'."
exit 1
fi
if [[ -z "$ACTOR_ID" || -z "$ACTOR_API_ID" || "$ACTOR_ID" != "$ACTOR_API_ID" ]]; then
echo "❌ ERROR: actor_id mismatch (context=$ACTOR_ID, api=$ACTOR_API_ID)."
exit 1
fi
echo "✓ Verified dispatcher is aspire-repo-bot GitHub App (type=Bot, id=$ACTOR_API_ID) — skipping human permission check."
exit 0
fi
# Get the user's permission level for this repo
PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/$ACTOR/permission --jq '.permission')
echo "User permission level: $PERMISSION"
# Only allow admin or maintain permission levels
if [[ "$PERMISSION" != "admin" && "$PERMISSION" != "maintain" ]]; then
echo "❌ ERROR: User $ACTOR does not have sufficient permissions."
echo "Required: 'admin' or 'maintain' permission level."
echo "Current: '$PERMISSION'"
exit 1
fi
echo "✓ User $ACTOR is authorized (permission: $PERMISSION)"
# --- Validate ----------------------------------------------------------
- name: Print Parameters
run: |
echo "=== Release Workflow Parameters ==="
echo "Release Version: ${{ inputs.release_version }}"
echo "Commit SHA: ${{ inputs.commit_sha }}"
echo "Release Branch: ${{ inputs.release_branch }}"
echo "Is Prerelease: ${{ inputs.is_prerelease }}"
echo "Dry Run: ${{ inputs.dry_run }}"
echo "Skip Tagging: ${{ inputs.skip_tagging }}"
echo "Skip GitHub Release: ${{ inputs.skip_github_release }}"
echo "Skip Merge PR: ${{ inputs.skip_merge_pr }}"
echo "Skip Baseline PR: ${{ inputs.skip_baseline_pr }}"
echo "==================================="
if [ "${{ inputs.dry_run }}" == "true" ]; then
echo ""
echo "⚠️ DRY RUN MODE ENABLED"
echo " All operations will be simulated - no actual changes will be made."
echo ""
fi
- name: Validate Version Format
run: |
VERSION="${{ inputs.release_version }}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?$ ]]; then
echo "❌ Invalid version format: $VERSION"
echo "Expected format: major.minor.patch[-prerelease]"
exit 1
fi
echo "✓ Version format is valid: $VERSION"
- name: Validate Commit SHA
run: |
SHA="${{ inputs.commit_sha }}"
if [[ ! "$SHA" =~ ^[a-f0-9]{40}$ ]]; then
echo "❌ Invalid commit SHA format: $SHA"
echo "Expected a 40-character hex string"
exit 1
fi
echo "✓ Commit SHA format is valid: $SHA"
# --- App token and checkout (shared by all subsequent steps) ----------
# Mint the aspire-repo-bot App installation token. Used by every step
# that needs to fire a downstream event (release create, PR create,
# branch push). Using the App token instead of the default GITHUB_TOKEN
# causes the `release: released` event to actually trigger
# release-notes-generate.lock.yml (events fired by GITHUB_TOKEN do not
# cascade into other workflow runs).
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ASPIRE_BOT_APP_ID }}
private-key: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }}
# Single checkout with full history. The tag-creation step needs all
# branches/tags reachable (fetch-depth: 0). The baseline-pr step
# explicitly switches to main before modifying files.
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
# --- Create Tag --------------------------------------------------------
- name: Check if Tag Exists
id: check-tag
if: inputs.skip_tagging != true
run: |
TAG_NAME="v${{ inputs.release_version }}"
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
EXISTING_SHA=$(git rev-parse "$TAG_NAME")
if [ "$EXISTING_SHA" == "${{ inputs.commit_sha }}" ]; then
echo "✓ Tag $TAG_NAME already exists and points to the correct commit"
echo "tag_exists=true" >> $GITHUB_OUTPUT
echo "tag_matches=true" >> $GITHUB_OUTPUT
else
echo "⚠️ Tag $TAG_NAME exists but points to different commit!"
echo " Existing: $EXISTING_SHA"
echo " Expected: ${{ inputs.commit_sha }}"
echo "tag_exists=true" >> $GITHUB_OUTPUT
echo "tag_matches=false" >> $GITHUB_OUTPUT
fi
else
echo "Tag $TAG_NAME does not exist yet"
echo "tag_exists=false" >> $GITHUB_OUTPUT
fi
- name: Fail if Tag Exists with Different SHA
if: inputs.skip_tagging != true && steps.check-tag.outputs.tag_exists == 'true' && steps.check-tag.outputs.tag_matches == 'false'
run: |
echo "❌ Tag already exists but points to a different commit!"
echo "This requires manual resolution."
exit 1
- name: Create and Push Tag
if: inputs.skip_tagging != true && steps.check-tag.outputs.tag_exists != 'true'
run: |
TAG_NAME="v${{ inputs.release_version }}"
DRY_RUN="${{ inputs.dry_run }}"
if [ "$DRY_RUN" == "true" ]; then
echo "🔍 [DRY RUN] Would create and push tag:"
echo " Tag name: $TAG_NAME"
echo " Target commit: ${{ inputs.commit_sha }}"
echo " Command: git tag \"$TAG_NAME\" \"${{ inputs.commit_sha }}\""
echo " Command: git push origin \"$TAG_NAME\""
else
git tag "$TAG_NAME" "${{ inputs.commit_sha }}"
git push origin "$TAG_NAME"
echo "✓ Created and pushed tag: $TAG_NAME"
fi
# --- Create GitHub Release --------------------------------------------
- name: Check if Release Exists
id: check-release
if: inputs.skip_github_release != true
env:
# Read-only check, no event fired — github.token is fine.
GH_TOKEN: ${{ github.token }}
run: |
TAG_NAME="v${{ inputs.release_version }}"
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "✓ Release $TAG_NAME already exists"
echo "release_exists=true" >> $GITHUB_OUTPUT
else
echo "Release $TAG_NAME does not exist yet"
echo "release_exists=false" >> $GITHUB_OUTPUT
fi
- name: Generate Release Notes
if: inputs.skip_github_release != true && steps.check-release.outputs.release_exists != 'true'
id: release-notes
run: |
# Write a short placeholder. The real release notes are generated
# asynchronously by the release-notes-generate agentic workflow,
# which fires on the `release: [released]` event once `gh release
# create` below publishes the release, and edits this body in
# place.
cat << EOF > release_notes.md
*Release notes are being generated automatically and will be added to this release shortly. If they haven't appeared within a few hours, ping the Aspire team.*
---
*Full commit: [${{ inputs.commit_sha }}](https://github.com/${{ github.repository }}/commit/${{ inputs.commit_sha }})*
EOF
echo "Placeholder release notes generated"
- name: Create GitHub Release
if: inputs.skip_github_release != true && steps.check-release.outputs.release_exists != 'true'
env:
# Use the App token here so the `release: released` event is
# authored by aspire-repo-bot and downstream workflows
# (release-notes-generate.lock.yml) get triggered. Events fired by
# GITHUB_TOKEN intentionally do NOT trigger other workflows.
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
TAG_NAME="v${{ inputs.release_version }}"
DRY_RUN="${{ inputs.dry_run }}"
PRERELEASE_FLAG=""
if [ "${{ inputs.is_prerelease }}" == "true" ]; then
PRERELEASE_FLAG="--prerelease"
fi
if [ "$DRY_RUN" == "true" ]; then
echo "🔍 [DRY RUN] Would create GitHub Release:"
echo " Tag: $TAG_NAME"
echo " Title: Aspire ${{ inputs.release_version }}"
echo " Target: ${{ inputs.commit_sha }}"
echo " Prerelease: ${{ inputs.is_prerelease }}"
echo ""
echo " Release notes content:"
echo " ─────────────────────────────────────"
cat release_notes.md
echo " ─────────────────────────────────────"
else
gh release create "$TAG_NAME" \
--title "Aspire ${{ inputs.release_version }}" \
--notes-file release_notes.md \
--target "${{ inputs.commit_sha }}" \
$PRERELEASE_FLAG
echo "✓ Created GitHub Release: $TAG_NAME"
fi
# ----------------------------------------------------------------------------
# merge-pr: runs in parallel with baseline-pr after the release job. A
# failure here must NOT block baseline-pr (and vice versa) — that matches
# the pre-consolidation per-job design.
# ----------------------------------------------------------------------------
merge-pr:
name: Create Merge-Back PR
needs: release
if: inputs.skip_merge_pr != true
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ASPIRE_BOT_APP_ID }}
private-key: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }}
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Check for Existing Merge PR
id: check-merge-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
RELEASE_BRANCH="${{ inputs.release_branch }}"
EXISTING_PR=$(gh pr list --head "$RELEASE_BRANCH" --base main --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "✓ Merge PR already exists: #$EXISTING_PR"
echo "pr_exists=true" >> $GITHUB_OUTPUT
echo "pr_number=$EXISTING_PR" >> $GITHUB_OUTPUT
else
echo "No existing merge PR found"
echo "pr_exists=false" >> $GITHUB_OUTPUT
fi
- name: Dry Run - Show Merge PR Details
if: steps.check-merge-pr.outputs.pr_exists != 'true' && inputs.dry_run == true
run: |
echo "🔍 [DRY RUN] Would create merge PR:"
echo " Title: Merge ${{ inputs.release_branch }} to main after v${{ inputs.release_version }} release"
echo " Head branch: ${{ inputs.release_branch }}"
echo " Base branch: main"
echo " Labels: area-engineering-systems"
echo ""
echo " PR body:"
echo " ─────────────────────────────────────"
echo " This PR merges the \`${{ inputs.release_branch }}\` branch back to \`main\` after the v${{ inputs.release_version }} release."
echo ""
echo " ## Checklist"
echo " - [ ] Verify all release-specific changes are appropriate for main"
echo " - [ ] Resolve any merge conflicts"
echo " - [ ] Ensure CI passes"
echo " ─────────────────────────────────────"
- name: Create Merge PR
if: steps.check-merge-pr.outputs.pr_exists != 'true' && inputs.dry_run != true
uses: ./.github/actions/create-pull-request
with:
token: ${{ steps.app-token.outputs.token }}
title: "Merge ${{ inputs.release_branch }} to main after v${{ inputs.release_version }} release"
body: |
This PR merges the `${{ inputs.release_branch }}` branch back to `main` after the v${{ inputs.release_version }} release.
## Checklist
- [ ] Verify all release-specific changes are appropriate for main
- [ ] Resolve any merge conflicts
- [ ] Ensure CI passes
---
*Created automatically by the release workflow.*
branch: ${{ inputs.release_branch }}
base: main
branch-already-exists: 'true'
labels: |
area-engineering-systems
# ----------------------------------------------------------------------------
# baseline-pr: runs in parallel with merge-pr after the release job.
# ----------------------------------------------------------------------------
baseline-pr:
name: Create Baseline Version PR
needs: release
if: inputs.skip_baseline_pr != true && inputs.is_prerelease != true
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ASPIRE_BOT_APP_ID }}
private-key: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }}
# Checkout main directly — the baseline-update branch is meant to be
# branched off main, not off the release branch the workflow was
# dispatched from.
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Check for Existing Baseline PR
id: check-baseline-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
BRANCH_NAME="update-baseline-${{ inputs.release_version }}"
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "✓ Baseline update PR already exists: #$EXISTING_PR"
echo "pr_exists=true" >> $GITHUB_OUTPUT
echo "pr_number=$EXISTING_PR" >> $GITHUB_OUTPUT
else
echo "No existing baseline update PR found"
echo "pr_exists=false" >> $GITHUB_OUTPUT
fi
- name: Update PackageValidationBaselineVersion
if: steps.check-baseline-pr.outputs.pr_exists != 'true'
run: |
VERSION="${{ inputs.release_version }}"
FILE="src/Directory.Build.props"
echo "Updating PackageValidationBaselineVersion to $VERSION in $FILE"
# Use sed to update the version - pattern matches the actual format in Directory.Build.props
# Format: <PackageValidationBaselineVersion Condition="'$(EnablePackageValidation)' == 'true' and '$(PackageValidationBaselineVersion)' == ''">VERSION</PackageValidationBaselineVersion>
sed -i -E "s#(<PackageValidationBaselineVersion[^>]*>)[^<]*(</PackageValidationBaselineVersion>)#\1$VERSION\2#" "$FILE"
echo "✓ Updated $FILE"
echo ""
echo "Changes made:"
if git diff --quiet "$FILE"; then
echo "::error::No changes detected in $FILE - PackageValidationBaselineVersion element may be missing or pattern did not match"
exit 1
fi
git diff "$FILE"
- name: Dry Run - Show Baseline PR Details
if: steps.check-baseline-pr.outputs.pr_exists != 'true' && inputs.dry_run == true
run: |
VERSION="${{ inputs.release_version }}"
BRANCH_NAME="update-baseline-$VERSION"
echo "🔍 [DRY RUN] Would create baseline version PR:"
echo " Title: Update PackageValidationBaselineVersion to $VERSION"
echo " Branch: $BRANCH_NAME"
echo " Base: main"
echo " Labels: area-engineering-systems"
echo ""
echo " File changes (src/Directory.Build.props):"
echo " ─────────────────────────────────────"
git diff src/Directory.Build.props || echo " (diff shown above)"
echo " ─────────────────────────────────────"
echo ""
echo " Commit message: Update PackageValidationBaselineVersion to $VERSION"
- name: Create Branch and Commit for Baseline PR
if: steps.check-baseline-pr.outputs.pr_exists != 'true' && inputs.dry_run != true
run: |
BRANCH_NAME="update-baseline-${{ inputs.release_version }}"
VERSION="${{ inputs.release_version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH_NAME"
git add src/Directory.Build.props
git commit -m "Update PackageValidationBaselineVersion to $VERSION"
git push origin "$BRANCH_NAME"
echo "✓ Created branch: $BRANCH_NAME"
- name: Create Baseline PR
if: steps.check-baseline-pr.outputs.pr_exists != 'true' && inputs.dry_run != true
uses: ./.github/actions/create-pull-request
with:
token: ${{ steps.app-token.outputs.token }}
title: "Update PackageValidationBaselineVersion to ${{ inputs.release_version }}"
body: |
This PR updates the `PackageValidationBaselineVersion` to `${{ inputs.release_version }}` after the release.
This ensures that future builds validate API compatibility against this release.
## Changes
- Updated `src/Directory.Build.props` with new baseline version
---
*Created automatically by the release workflow.*
branch: update-baseline-${{ inputs.release_version }}
base: main
branch-already-exists: 'true'
labels: |
area-engineering-systems
# ----------------------------------------------------------------------------
# summary: joins all upstream jobs with `if: always()` and reports the
# final outcome. Uses `needs.*.result` to surface per-job status because
# the work is no longer in a single job.
# ----------------------------------------------------------------------------
summary:
name: Summary
needs: [release, merge-pr, baseline-pr]
if: always()
runs-on: ubuntu-latest
steps:
- name: Print Summary
run: |
echo ""
if [ "${{ inputs.dry_run }}" == "true" ]; then
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ 🔍 DRY RUN - NO CHANGES WERE MADE 🔍 ║"
echo "╠═══════════════════════════════════════════════════════════════╣"
else
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ GITHUB RELEASE SUMMARY ║"
echo "╠═══════════════════════════════════════════════════════════════╣"
fi
echo "║ Version: ${{ inputs.release_version }}"
echo "║ Commit SHA: ${{ inputs.commit_sha }}"
echo "║ Release Branch: ${{ inputs.release_branch }}"
echo "║ Is Prerelease: ${{ inputs.is_prerelease }}"
echo "║ Dry Run: ${{ inputs.dry_run }}"
echo "║ Skip Tagging: ${{ inputs.skip_tagging }}"
echo "║ Skip GitHub Release: ${{ inputs.skip_github_release }}"
echo "║ Skip Merge PR: ${{ inputs.skip_merge_pr }}"
echo "║ Skip Baseline PR: ${{ inputs.skip_baseline_pr }}"
echo "╠═══════════════════════════════════════════════════════════════╣"
echo "║ Release job: ${{ needs.release.result }}"
echo "║ Merge PR job: ${{ needs.merge-pr.result }}"
echo "║ Baseline PR job: ${{ needs.baseline-pr.result }}"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo ""
- name: Create Job Summary
run: |
DRY_RUN_BANNER=""
if [ "${{ inputs.dry_run }}" == "true" ]; then
DRY_RUN_BANNER="## 🔍 DRY RUN MODE - No changes were made
This was a dry run execution. All validations and checks were performed, but no tags, releases, or PRs were created.
---
"
fi
# Map a needs.*.result string to an icon for the summary table.
render_status() {
case "$1" in
success) echo "✅ \`success\`" ;;
failure) echo "❌ \`failure\`" ;;
cancelled) echo "⏹️ \`cancelled\`" ;;
skipped) echo "⏭️ \`skipped\`" ;;
*) echo "❔ \`$1\`" ;;
esac
}
RELEASE_STATUS=$(render_status "${{ needs.release.result }}")
MERGE_STATUS=$(render_status "${{ needs.merge-pr.result }}")
BASELINE_STATUS=$(render_status "${{ needs.baseline-pr.result }}")
cat << EOF >> $GITHUB_STEP_SUMMARY
# Release Summary: v${{ inputs.release_version }}
${DRY_RUN_BANNER}## Job results
| Job | Result |
|-----|--------|
| Release Tasks (tag + GitHub release) | ${RELEASE_STATUS} |
| Create Merge-Back PR | ${MERGE_STATUS} |
| Create Baseline Version PR | ${BASELINE_STATUS} |
## Details
- **Tag**: v${{ inputs.release_version }}
- **Commit**: \`${{ inputs.commit_sha }}\`
- **Branch**: ${{ inputs.release_branch }}
- **Prerelease**: ${{ inputs.is_prerelease }}
- **Dry Run**: ${{ inputs.dry_run }}
- **Skip Tagging**: ${{ inputs.skip_tagging }}
- **Skip GitHub Release**: ${{ inputs.skip_github_release }}
- **Skip Merge PR**: ${{ inputs.skip_merge_pr }}
- **Skip Baseline PR**: ${{ inputs.skip_baseline_pr }}
## Links
- [Release](https://github.com/${{ github.repository }}/releases/tag/v${{ inputs.release_version }})
- [Tag](https://github.com/${{ github.repository }}/tree/v${{ inputs.release_version }})
EOF