Skip to content

Create Release with Changelog #43

Create Release with Changelog

Create Release with Changelog #43

name: Create Release with Changelog
# This workflow automatically creates GitHub releases with changelogs
# extracted from changelog.md or generated from git commits.
on:
workflow_run:
workflows: ["Publish to PyPI"]
types:
- completed
workflow_dispatch: # Allows manual trigger
inputs:
version:
description: 'Version to create release for (e.g., 2025.10.15)'
required: false
type: string
jobs:
create-release:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Get full history for changelog generation
- name: Get version
id: get_version
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
else
VERSION=$(grep -oP '__version__ = "\K[^"]*' webscout/version.py)
fi
# Normalize version: strip leading 'v' or 'V' if user provided it
VERSION="${VERSION#v}"
VERSION="${VERSION#V}"
# If the user passed a date-like version with hyphens (e.g. 2025-11-16), normalize to dots (2025.11.16)
if echo "$VERSION" | grep -E '^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$' >/dev/null; then
VERSION=$(echo "$VERSION" | sed 's/-/./g')
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Check if tag exists
id: check_tag
run: |
if git rev-parse "v${{ steps.get_version.outputs.version }}" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Tag v${{ steps.get_version.outputs.version }} already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Tag v${{ steps.get_version.outputs.version }} does not exist"
fi
- name: Get previous tag
id: previous_tag
if: steps.check_tag.outputs.exists == 'false'
run: |
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
echo "previous_tag=" >> $GITHUB_OUTPUT
echo "No previous tag found"
else
echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
echo "Previous tag: $PREVIOUS_TAG"
fi
- name: Extract changelog from changelog.md
id: changelog
if: steps.check_tag.outputs.exists == 'false'
run: |
VERSION="${{ steps.get_version.outputs.version }}"
PREVIOUS_TAG="${{ steps.previous_tag.outputs.previous_tag }}"
echo "📝 Extracting changelog..."
# Determine changelog filename (support uppercase and lowercase file names)
CHANGELOG_FILE=""
if [ -f "changelog.md" ]; then
CHANGELOG_FILE="changelog.md"
elif [ -f "CHANGELOG.md" ]; then
CHANGELOG_FILE="CHANGELOG.md"
fi
if [ -z "$CHANGELOG_FILE" ]; then
echo "Warning: changelog file not found, generating from git commits"
else
# Convert UTF-16 to UTF-8 if needed
if file "$CHANGELOG_FILE" | grep -q "UTF-16"; then
iconv -f UTF-16LE -t UTF-8 "$CHANGELOG_FILE" > /tmp/changelog_utf8.md
CHANGELOG_FILE="/tmp/changelog_utf8.md"
fi
# If version is date-like (YYYY.MM.DD) produce a zero-padded variant for matching
VERSION_PAD="$VERSION"
VERSION_STRIP="$VERSION"
if echo "$VERSION" | grep -E '^[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}$' >/dev/null; then
YEAR="$(echo "$VERSION" | cut -d. -f1)"
MONTH="$(echo "$VERSION" | cut -d. -f2)"
DAY="$(echo "$VERSION" | cut -d. -f3)"
if [ ${#DAY} -eq 1 ]; then
DAY_PAD="0$DAY"
else
DAY_PAD="$DAY"
fi
VERSION_PAD="$YEAR.$MONTH.$DAY_PAD"
# strip leading zeros for the day (e.g. 07 -> 7) to match alternate formatting
DAY_STRIPPED="$(echo "$DAY" | sed 's/^0*//')"
VERSION_STRIP="$YEAR.$MONTH.$DAY_STRIPPED"
fi
echo "Attempting to extract changelog for version: $VERSION (also trying $VERSION_PAD) from $CHANGELOG_FILE"
# Create dot/hyphen-tolerant regex patterns from version strings for robust matching
VER_PAT=$(printf "%s" "$VERSION" | sed 's/\./[.-]/g')
VER_PAD_PAT=$(printf "%s" "$VERSION_PAD" | sed 's/\./[.-]/g')
VER_STRIP_PAT=$(printf "%s" "$VERSION_STRIP" | sed 's/\./[.-]/g')
# Use awk with a regex that matches any of these patterns in bracketed or unbracketed forms,
# with optional "v" prefix and either dot or hyphen separators (e.g., 2025.11.16 or 2025-11-16).
awk -v verpat="$VER_PAT" -v verpadpat="$VER_PAD_PAT" -v verstrippat="$VER_STRIP_PAT" '
BEGIN {
found=0;
patt = "(" verpat "|" verpadpat "|" verstrippat ")";
# Bracketed: e.g. "## [v2025.11.16]" or "## [2025-11-16]"
bracketed = "^##[[:space:]]*\\[[[:space:]]*v?" patt "[[:space:]]*\\]";
# Unbracketed: e.g. "## v2025.11.16" or "## 2025-11-16"
unbracketed = "^##[[:space:]]*v?" patt "($|[[:space:]]|-)";
pattern = bracketed "|" unbracketed;
}
{
if (!found) {
if ($0 ~ pattern) { found = 1; print; next }
} else {
if ($0 ~ "^##[[:space:]]") exit; print
}
}' "$CHANGELOG_FILE" > RELEASE_NOTES.md
fi
# If no content was extracted, generate from git commits
if [ ! -s RELEASE_NOTES.md ]; then
echo "Warning: Could not find changelog entry for v$VERSION"
echo "Generating basic changelog from git commits..."
TIMESTAMP=$(date -u +%Y-%m-%d)
echo "## [$VERSION] - $TIMESTAMP" > RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
if [ -z "$PREVIOUS_TAG" ]; then
echo "📝 **Initial Release**" >> RELEASE_NOTES.md
else
echo "📝 **Changes since $PREVIOUS_TAG**" >> RELEASE_NOTES.md
echo "" >> RELEASE_NOTES.md
echo "### 📦 Changes" >> RELEASE_NOTES.md
git log $PREVIOUS_TAG..HEAD --pretty=format:"- %s (%h)" >> RELEASE_NOTES.md
fi
echo "" >> RELEASE_NOTES.md
echo "### 📦 Installation" >> RELEASE_NOTES.md
echo '```bash' >> RELEASE_NOTES.md
echo "pip install --upgrade webscout==$VERSION" >> RELEASE_NOTES.md
echo '```' >> RELEASE_NOTES.md
fi
# Output for GitHub Actions
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat RELEASE_NOTES.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Git Tag
if: steps.check_tag.outputs.exists == 'false'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git tag -a "v${{ steps.get_version.outputs.version }}" -m "Release v${{ steps.get_version.outputs.version }}"
git push origin "v${{ steps.get_version.outputs.version }}"
- name: Create GitHub Release
if: steps.check_tag.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.get_version.outputs.version }}
name: Release v${{ steps.get_version.outputs.version }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload changelog artifact
if: steps.check_tag.outputs.exists == 'false'
uses: actions/upload-artifact@v5
with:
name: changelog-v${{ steps.get_version.outputs.version }}
path: RELEASE_NOTES.md
retention-days: 90
- name: Comment on related PRs
if: steps.check_tag.outputs.exists == 'false'
uses: actions/github-script@v7
with:
script: |
const version = '${{ steps.get_version.outputs.version }}';
const previousTag = '${{ steps.previous_tag.outputs.previous_tag }}';
if (!previousTag) {
console.log('No previous tag, skipping PR comments');
return;
}
// Get commits in this release
const commits = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: previousTag,
head: 'HEAD'
});
// Extract PR numbers from commit messages
const prNumbers = new Set();
for (const commit of commits.data.commits) {
const match = commit.commit.message.match(/#(\d+)/g);
if (match) {
match.forEach(pr => prNumbers.add(parseInt(pr.slice(1))));
}
}
// Comment on each PR
for (const prNumber of prNumbers) {
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `🎉 This PR is included in [v${version}](https://github.com/${context.repo.owner}/${context.repo.repo}/releases/tag/v${version})!\n\nInstall it with:\n\`\`\`bash\npip install --upgrade webscout==${version}\n\`\`\``
});
console.log(`Commented on PR #${prNumber}`);
} catch (error) {
console.log(`Failed to comment on PR #${prNumber}: ${error.message}`);
}
}
- name: Notification
if: steps.check_tag.outputs.exists == 'false'
run: |
echo "✅ Successfully created release v${{ steps.get_version.outputs.version }} with changelog!"
echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.get_version.outputs.version }}"
- name: Skip notification
if: steps.check_tag.outputs.exists == 'true'
run: |
echo "ℹ️ Release v${{ steps.get_version.outputs.version }} already exists, skipping creation"