name: Restore Release
on:
workflow_dispatch:
inputs:
mode:
description: 'Release mode'
required: true
default: 'single'
type: choice
options:
- single
- all-missing
version:
description: 'Version to release (required for "single" mode, e.g. 0.1.7)'
required: false
default: ''
type: string
commit_sha:
description: 'Optional commit SHA (auto-detected if blank)'
required: false
default: ''
type: string
env:
CARGO_TERM_COLOR: always
jobs:
restore:
name: ${{ inputs.mode == 'all-missing' && 'Restore all missing releases' || format('Restore v{0}', inputs.version) }}
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
versions: ${{ steps.plan.outputs.versions }}
count: ${{ steps.plan.outputs.count }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Parse CHANGELOG versions
id: parse
run: |
VERSIONS=$(grep -oP '^## \[\K[0-9]+\.[0-9]+\.[0-9a-zA-Z.-]+(?=\])' CHANGELOG.md | sort -V | uniq)
echo "versions<<EOF" >> "$GITHUB_OUTPUT"
echo "$VERSIONS" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$VERSIONS" | wc -l)
echo "Found $COUNT versions in CHANGELOG:"
echo "$VERSIONS" | tr '\n' ' '
echo ""
- name: Determine versions to restore
id: plan
run: |
if [ "${{ inputs.mode }}" = "single" ]; then
VERSION="${{ inputs.version }}"
if [ -z "$VERSION" ]; then
echo "❌ version input is required in 'single' mode"
exit 1
fi
echo "versions=[\"$VERSION\"]" >> "$GITHUB_OUTPUT"
echo "count=1" >> "$GITHUB_OUTPUT"
exit 0
fi
MISSING=""
COUNT=0
while IFS= read -r ver; do
[ -z "$ver" ] && continue
if git rev-parse "v$ver" &>/dev/null 2>&1; then
echo "✅ v$ver — already tagged, skipping"
else
MISSING="$MISSING$ver"$'\n'
COUNT=$((COUNT + 1))
echo "❌ v$ver — missing, will restore"
fi
done <<< "${{ steps.parse.outputs.versions }}"
MISSING=$(echo "$MISSING" | sed '/^$/d')
JSON_ARRAY=$(echo "$MISSING" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "versions=$JSON_ARRAY" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
- name: Summary
run: |
COUNT="${{ steps.plan.outputs.count }}"
if [ "$COUNT" = "0" ]; then
echo "🎉 All CHANGELOG versions already have tags! Nothing to restore."
else
echo "📋 Will restore $COUNT release(s)"
fi
create:
name: Create v${{ matrix.version }}
needs: [restore]
if: needs.restore.outputs.count != '0'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.restore.outputs.versions) }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Find target commit
id: find_commit
run: |
VERSION="${{ matrix.version }}"
if [ "${{ inputs.mode }}" = "single" ] && [ -n "${{ inputs.commit_sha }}" ]; then
echo "Using provided commit: ${{ inputs.commit_sha }}"
echo "sha=${{ inputs.commit_sha }}" >> "$GITHUB_OUTPUT"
exit 0
fi
FOUND=$(git log --oneline --all --grep="chore(release): v$VERSION" --format="%H" | head -1)
if [ -n "$FOUND" ]; then
echo "Found release commit: $FOUND"
echo "sha=$FOUND" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Searching Cargo.toml version history for v$VERSION..."
FOUND=$(git log --all -G"^version = \"$VERSION\"" --format="%H" -- Cargo.toml | tail -1)
if [ -n "$FOUND" ]; then
echo "Found commit via Cargo.toml: $FOUND"
echo "sha=$FOUND" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "⚠️ Could not find a commit for v$VERSION — skipping"
echo "sha=" >> "$GITHUB_OUTPUT"
- name: Extract CHANGELOG section
id: changelog
run: |
VERSION="${{ matrix.version }}"
NOTES_FILE="${RUNNER_TEMP}/release-notes.md"
SECTION=$(sed -n "/^## \\[$VERSION\\]/,/^---/p" CHANGELOG.md | tail -n +2 | sed \$d)
if [ -z "$(echo "$SECTION" | tr -d ' \\n\\r')" ]; then
SECTION=$(sed -n "/^## \\[$VERSION\\]/,/^## \\[/p" CHANGELOG.md | head -n -1 | tail -n +2)
fi
if [ -z "$(echo "$SECTION" | tr -d ' \\n\\r')" ]; then
echo "⚪ No CHANGELOG content found — using default notes"
echo "Release v${VERSION} of Forge Guard." > "$NOTES_FILE"
else
REPO="${{ github.repository }}"
echo "$SECTION" > "$NOTES_FILE"
echo "" >> "$NOTES_FILE"
echo "---" >> "$NOTES_FILE"
PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]' | awk -v ver="v$VERSION" '$0 < ver {print; exit}')
if [ -z "$PREV_TAG" ]; then
echo "**Full Changelog**: https://github.com/$REPO/releases/tag/v${VERSION}" >> "$NOTES_FILE"
else
echo "**Full Changelog**: https://github.com/$REPO/compare/$PREV_TAG...v${VERSION}" >> "$NOTES_FILE"
fi
fi
echo "notes_file=$NOTES_FILE" >> "$GITHUB_OUTPUT"
- name: Create tag and GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ matrix.version }}"
TAG_NAME="v$VERSION"
COMMIT="${{ steps.find_commit.outputs.sha }}"
NOTES_FILE="${{ steps.changelog.outputs.notes_file }}"
if [ -z "$COMMIT" ]; then
echo "⚠️ No commit found for v$VERSION — skipping release"
exit 0
fi
if gh release view "$TAG_NAME" --json tagName --jq '.tagName' &>/dev/null; then
echo "ℹ️ Release $TAG_NAME already exists — updating notes"
gh release edit "$TAG_NAME" \
--notes-file "$NOTES_FILE" \
--title "Forge Guard v${VERSION}"
echo "✅ Release $TAG_NAME updated"
else
echo "Creating Release $TAG_NAME at $COMMIT..."
gh release create "$TAG_NAME" \
--title "Forge Guard v${VERSION}" \
--notes-file "$NOTES_FILE" \
--target "$COMMIT"
echo "✅ Release $TAG_NAME created"
fi
finalize:
name: Set latest release
needs: [restore, create]
if: always() && needs.restore.result == 'success' && needs.restore.outputs.count != '0'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Determine highest restored version
id: highest
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSIONS='${{ needs.restore.outputs.versions }}'
echo "Restored versions: $VERSIONS"
HIGHEST=$(echo "$VERSIONS" | jq -r '.[]' | sort -V | tail -1)
echo "Highest version: $HIGHEST"
echo "tag=v$HIGHEST" >> "$GITHUB_OUTPUT"
- name: Mark as latest release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.highest.outputs.tag }}"
IS_PRERELEASE=$(gh release view "$TAG" --json isPrerelease --jq '.isPrerelease' 2>/dev/null || echo "true")
IS_DRAFT=$(gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null || echo "true")
echo "$TAG: prerelease=$IS_PRERELEASE, draft=$IS_DRAFT"
# Convert to full release if needed, then mark as latest
if [ "$IS_PRERELEASE" = "true" ] || [ "$IS_DRAFT" = "true" ]; then
echo "Converting $TAG to a full release..."
gh release edit "$TAG" --draft=false --prerelease=false
fi
echo "Marking $TAG as the latest release..."
gh release edit "$TAG" --latest
echo "✅ $TAG is now the latest release"