Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changelog Automation for Squash and Merge commit messages #28747

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
136 changes: 136 additions & 0 deletions .github/workflows/update_changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Update Changelog with new commit

permissions:
pull-requests: write
contents: write

on:
pull_request:
# Inputs the workflow accepts.
types: [closed]

jobs:
pull-commit-message:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
outputs:
message: ${{ steps.pull.outputs.message }}
steps:
- uses: actions/checkout@v4
continue-on-error: true
- name: get merge commit message
id: pull
run: |
pull_number="$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")"
commit_message="$(git log --pretty="format:%b")"
echo message="$commit_message [GH-$pull_number]" >> $GITHUB_OUTPUT
# check-for-changelog-entry
changelog-entry:
# if contains to check for bug, enhancement, feature
if: ${{ contains(needs.pull-commit-message.outputs.message, '[BUG]') || contains(needs.pull-commit-message.outputs.message, '[ENHANCEMENT]') || contains(needs.pull-commit-message.outputs.message, '[FEATURE]') }}
runs-on: ubuntu-latest
needs: pull-commit-message
outputs:
optIn: ${{ steps.in.outputs.bool }}
entry: ${{ needs.pull-commit-message.outputs.message }}
steps:
- name: changelog entry opt in
id: in
continue-on-error: true
run: echo "opted in to changelog entry" | echo bool="true" >> $GITHUB_OUTPUT
# if there is a changelog entry, check for PR Open
update-changelog:
if: needs.changelog-entry.outputs.optIn
runs-on: ubuntu-latest
needs: changelog-entry
steps:
- name: Check if PR exists
id: check
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
prs=$(gh pr list \
--repo "$GITHUB_REPOSITORY" \
--json title \
--label "changelog" \
--jq 'length')
if [[ $prs -gt 0 ]]; then
echo "existing=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
- name: check for branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
run: |
BRANCH=automated-changelog
if gh api repos/hashicorp/terraform-provider-azurerm/branches/$BRANCH > /dev/null 2>&1; then
echo "Branch exists on remote..."
git fetch origin $BRANCH
git checkout $BRANCH
else
echo "Branch does not exist on remote, creating locally..."
git checkout -b $BRANCH
fi

- name: Create pull request
#if changelog PR isn't already open, open one
#create a new PR, start with appending the release number and (unreleased)
if: '!steps.check.outputs.existing'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
run: |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"

#new pull request for new release needs the headers all added to the top
FILE="CHANGELOG.md"
version=$(head -n 1 "$FILE")
IFS='.' read major minor patch <<< "$version"
((minor++))
patch=$(echo $patch | sed 's/ (.*)//')
new_version="${major}.$minor.${patch} (Unreleased)"
headers="${new_version}\n\nENHANCEMENTS:\n\nFEATURES:\n\nBUG FIXES:\n"
temp_file=$(mktemp)
echo -e "$headers" > "$temp_file"
cat "$FILE" >> "$temp_file"
mv "$temp_file" "$FILE"
echo "File has been updated."

major=$(echo $major | sed 's/## //')
RELEASENUM="${major}.$minor.${patch}"

git add CHANGELOG.md
git commit -m "staring new changelog PR"
git push --set-upstream origin automated-changelog
echo "Creating a new pull request"
gh pr create \
--repo "$GITHUB_REPOSITORY" \
--base main \
--head automated-changelog \
-l "changelog" \
-t "CHANGELOG.md for $RELEASENUM" \
-b "Automated changelog for next release, $RELEASENUM"

- name: Set up Go
uses: actions/setup-go@v3 # Set up go
with:
go-version: '1.20'

- name: Add commit message to changelog pull request
# at this point a PR is opened for sure, now add entry
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
run: |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"

go run scripts/update_changelog.go CHANGELOG.md '${{ needs.changelog-entry.outputs.entry }}'

git add CHANGELOG.md
git commit -m "Update changelog"
git push --set-upstream origin automated-changelog

128 changes: 128 additions & 0 deletions scripts/update_changelog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package main

import (
"bufio"
"fmt"
"os"
"strings"
)

// Function to find the header and return its index
func findHeaderIndex(lines []string, header string) int {
for i, line := range lines {
if strings.HasPrefix(line, header) {
return i
}
}
return -1
}

// Function to append the new entry under the appropriate header in alphabetical order
func appendUnderHeader(filePath string, newEntry, header string) error {
// Open the file for reading and appending
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
defer file.Close()

// Read the file content
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}

if err := scanner.Err(); err != nil {
return err
}

// Find the correct header section
headerIndex := findHeaderIndex(lines, header)

// Now append the new entry under the correct header
// Check if the next line is empty or not for proper formatting
insertIndex := headerIndex + 1
for i := headerIndex + 1; i < len(lines); i++ {
// Look for the next header to break the section
if strings.HasPrefix(lines[i], "[") {
insertIndex = i
break
}
}

// Remove the header prefix from the new entry
// Trim the header prefix based on which one it matches
var trimmedEntry string
if strings.HasPrefix(newEntry, "[BUG]") {
trimmedEntry = strings.TrimPrefix(newEntry, "[BUG] ")
} else if strings.HasPrefix(newEntry, "[ENHANCEMENT]") {
trimmedEntry = strings.TrimPrefix(newEntry, "[ENHANCEMENT] ")
} else if strings.HasPrefix(newEntry, "[FEATURE]") {
trimmedEntry = strings.TrimPrefix(newEntry, "[FEATURE] ")
} else {
// If the entry doesn't match one of the expected headers, print an error
fmt.Println("Error: New entry must start with one of the headers [BUG], [ENHANCEMENT], or [FEATURE].")
return nil
}

// Insert the new entry under the header
var section []string
for i := headerIndex + 1; i < insertIndex; i++ {
section = append(section, lines[i])
}
section = append(section, trimmedEntry)

// Rebuild the file content
lines = append(lines[:headerIndex+1], append(section, lines[insertIndex:]...)...)

// Open the file for writing and overwrite the content
file, err = os.OpenFile(filePath, os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer file.Close()

// Write the updated content back to the file
writer := bufio.NewWriter(file)
for _, line := range lines {
_, err := writer.WriteString(line + "\n")
if err != nil {
return err
}
}
return writer.Flush()
}

func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: go run update_changelog.go CHANGELOG.md <new_entry>")
return
}

filePath := os.Args[1]
newEntry := os.Args[2]

// Validate and determine the correct header for the new entry
var selectedHeader string
if strings.HasPrefix(newEntry, "[BUG]") {
selectedHeader = "BUG FIXES:"
} else if strings.HasPrefix(newEntry, "[ENHANCEMENT]") {
selectedHeader = "ENHANCEMENTS:"
} else if strings.HasPrefix(newEntry, "[FEATURE]") {
selectedHeader = "FEATURES:"
} else {
// If the entry doesn't match one of the expected headers, print an error
fmt.Println("Error: New entry must start with one of the headers [BUG], [ENHANCEMENT], or [FEATURE].")
return
}

// Call the function to append under the appropriate header
err := appendUnderHeader(filePath, newEntry, selectedHeader)
if err != nil {
fmt.Println("Error appending to file:", err)
return
}

fmt.Println("Successfully appended the new entry under the", selectedHeader, "header.")
}