Skip to content
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
139 changes: 139 additions & 0 deletions .github/workflows/changelog-generator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Generate Changelog

on:
milestone:
types: [closed]
workflow_dispatch:
inputs:
milestone:
description: 'Milestone number or title to generate changelog for'
required: true
type: string
dry_run:
description: 'Preview changelog without committing'
required: false
type: boolean
default: false

permissions:
contents: write
pull-requests: read

jobs:
generate-changelog:
name: Generate WordPress readme.txt Changelog
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer:v2

- name: Install dependencies
run: composer install --no-progress --prefer-dist --no-interaction

- name: Get milestone information
id: milestone
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MILESTONE_INPUT: ${{ github.event.inputs.milestone }}
EVENT_MILESTONE: ${{ github.event.milestone.number }}
run: |
if [ -n "$MILESTONE_INPUT" ]; then
MILESTONE="$MILESTONE_INPUT"
else
MILESTONE="$EVENT_MILESTONE"
fi

echo "milestone=$MILESTONE" >> $GITHUB_OUTPUT

# Fetch milestone details
MILESTONE_DATA=$(gh api "repos/${{ github.repository }}/milestones" \
--jq ". [] | select(.number == $MILESTONE or .title == \"$MILESTONE\")")

MILESTONE_TITLE=$(echo "$MILESTONE_DATA" | jq -r '.title')
MILESTONE_NUMBER=$(echo "$MILESTONE_DATA" | jq -r '.number')
DUE_DATE=$(echo "$MILESTONE_DATA" | jq -r '.due_on // empty')

echo "title=$MILESTONE_TITLE" >> $GITHUB_OUTPUT
echo "number=$MILESTONE_NUMBER" >> $GITHUB_OUTPUT

if [ -n "$DUE_DATE" ]; then
FORMATTED_DATE=$(date -d "$DUE_DATE" "+%d %b %Y")
echo "date=$FORMATTED_DATE" >> $GITHUB_OUTPUT
else
FORMATTED_DATE=$(date "+%d %b %Y")
echo "date=$FORMATTED_DATE" >> $GITHUB_OUTPUT
fi

- name: Fetch PRs from milestone
id: prs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MILESTONE_NUMBER: ${{ steps.milestone.outputs.number }}
run: |
# Fetch all merged PRs in this milestone
gh pr list \
--repo "${{ github.repository }}" \
--state merged \
--milestone "$MILESTONE_NUMBER" \
--limit 1000 \
--json number,title,labels,author,mergedAt \
> prs.json

echo "Found $(jq length prs.json) merged PRs in milestone"

- name: Generate changelog entry
id: changelog
env:
MILESTONE_TITLE: ${{ steps.milestone.outputs.title }}
RELEASE_DATE: ${{ steps.milestone.outputs.date }}
run: |
php bin/generate-changelog.php \
--milestone-title "$MILESTONE_TITLE" \
--release-date "$RELEASE_DATE" \
--prs-file prs.json \
--output changelog-entry.txt

echo "Generated changelog entry:"
cat changelog-entry.txt

- name: Update readme.txt
if: github.event.inputs.dry_run != 'true'
run: |
php bin/update-readme-changelog.php \
--readme readme.txt \
--changelog-entry changelog-entry.txt \
--output readme.txt

- name: Commit changes
if: github.event.inputs.dry_run != 'true'
env:
MILESTONE_TITLE: ${{ steps.milestone.outputs.title }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git add readme.txt

if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Update changelog for $MILESTONE_TITLE"
git push origin trunk
fi

- name: Display changelog preview
if: github.event.inputs.dry_run == 'true'
run: |
echo "# Changelog Preview (Dry Run)"
echo ""
cat changelog-entry.txt
93 changes: 93 additions & 0 deletions bin/generate-changelog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* Generate changelog entry from milestone PRs
*
* Usage: php bin/generate-changelog. php --milestone-title "6.8.1" --release-date "15 Jan 2026" --prs-file prs. json --output changelog-entry.txt
*/

$options = getopt('', [
'milestone-title:',
'release-date:',
'prs-file:',
'output: ',
]);

$milestone_title = $options['milestone-title'] ?? '';
$release_date = $options['release-date'] ?? date('d M Y');
$prs_file = $options['prs-file'] ?? '';
$output_file = $options['output'] ?? 'php://stdout';

if (empty($prs_file) || ! file_exists($prs_file)) {
fwrite(STDERR, "Error: PRs file not found\n");
exit(1);
}

$prs = json_decode(file_get_contents($prs_file), true);

if ($prs === null) {
fwrite(STDERR, "Error: Invalid JSON in PRs file\n");
exit(1);
}

// Categorize PRs by label
$categorized = [
'Features' => [],
'Enhancements' => [],
'Fixes' => [],
'Documentation' => [],
'Project Management' => [],
'Other' => [],
];

$label_mapping = [
'[Type] Feature' => 'Features',
'[Type] Enhancement' => 'Enhancements',
'[Type] Bug' => 'Fixes',
'documentation' => 'Documentation',
'[Type] Project Management' => 'Project Management',
];

foreach ($prs as $pr) {
$category = 'Other';

foreach ($pr['labels'] as $label) {
$label_name = $label['name'];
if (isset($label_mapping[$label_name])) {
$category = $label_mapping[$label_name];
break;
}
}

$categorized[$category][] = $pr;
}

// Generate changelog entry
$changelog = "= {$milestone_title} =\n";
$changelog .= "*Release Date {$release_date}*\n\n";

foreach ($categorized as $category => $items) {
if (empty($items)) {
continue;
}

$changelog .= "*{$category}*\n\n";

foreach ($items as $pr) {
$title = $pr['title'];
// Remove common prefixes
$title = preg_replace('/^(Fix|Add|Update|Remove|Improve|Create|Delete|Refactor):\s*/i', '', $title);
$title = ucfirst($title);

$changelog .= "- {$title}.\n";
}

$changelog .= "\n";
}

// Write output
if ($output_file === 'php://stdout') {
echo $changelog;
} else {
file_put_contents($output_file, $changelog);
fwrite(STDERR, "Changelog entry written to {$output_file}\n");
}
50 changes: 50 additions & 0 deletions bin/update-readme-changelog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<? php
/**
* Update readme.txt with new changelog entry
*
* Usage: php bin/update-readme-changelog.php --readme readme.txt --changelog-entry changelog-entry.txt --output readme.txt
*/

$options = getopt('', [
'readme:',
'changelog-entry:',
'output:',
]);

$readme_file = $options['readme'] ?? 'readme.txt';
$changelog_entry_file = $options['changelog-entry'] ?? '';
$output_file = $options['output'] ?? 'readme. txt';

if (! file_exists($readme_file)) {
fwrite(STDERR, "Error: readme. txt not found at {$readme_file}\n");
exit(1);
}

if (!file_exists($changelog_entry_file)) {
fwrite(STDERR, "Error: changelog entry file not found at {$changelog_entry_file}\n");
exit(1);
}

$readme_content = file_get_contents($readme_file);
$changelog_entry = file_get_contents($changelog_entry_file);

// Find the changelog section
$pattern = '/(== Changelog ==\s*\n\n)/';

if (preg_match($pattern, $readme_content, $matches, PREG_OFFSET_CAPTURE)) {
$insert_position = $matches[0][1] + strlen($matches[0][0]);

// Insert new changelog entry
$updated_content = substr_replace(
$readme_content,
$changelog_entry . "\n",
$insert_position,
0
);

file_put_contents($output_file, $updated_content);
echo "Successfully updated {$output_file}\n";
} else {
fwrite(STDERR, "Error: Could not find Changelog section in readme.txt\n");
exit(1);
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"docs:lint": "npm run lint:md",
"docs:manifest": "php docs/bin/generate-manifest.php",
"docs:parse": "php docs/bin/generate-parsed-md.php --output=${DOCS_OUTPUT_DIR:-docs/code-reference}",
"changelog:generate": "php bin/generate-changelog.php"
"format": [
"@format:php",
"@format:packages"
Expand Down
Loading