Create Release with Changelog #43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |