Skip to content

Commit e5db776

Browse files
authored
Changelogger: automatically create a changelog entry from info extracted from PR description (#1456)
* Split the changelog check workflow into 2 steps It will make it easier to insert another step to get data from the PR description. * Update Pull Request template to include changelog fields * Allow the workflow to run when PR is edited (description changed) * Add new step to extract changelog info from PR body * Create a changelog file from the PR description * Add changelog * Add newlines before changelog message description
1 parent eb96988 commit e5db776

File tree

3 files changed

+182
-6
lines changed

3 files changed

+182
-6
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,43 @@ Fixes #
2020
* Go to '..'
2121
*
2222

23+
### Changelog entry
24+
25+
<!-- You can optionally choose to enter a changelog entry by checking the box below and supplying data. -->
26+
<!-- It will trigger a GitHub workflow that will create and push the entry into the branch. -->
27+
28+
<!-- Due to org permissions, the job may fail for PRs crated from a fork under GitHub organizations. -->
29+
<!-- In this case, you can create entry manually with `composer changelog:add` and push it into the branch. -->
30+
31+
<!-- If no changelog entry is required for this PR, please add the "Skip Changelog" label. -->
32+
33+
- [ ] Automatically create a changelog entry from the details below.
34+
35+
<details>
36+
37+
<summary>Changelog Entry Details</summary>
38+
39+
#### Significance
40+
41+
<!-- Choose only one -->
42+
43+
- [ ] Patch
44+
- [ ] Minor
45+
- [ ] Major
46+
47+
#### Type
48+
49+
<!-- Choose only one -->
50+
51+
- [ ] Added - for new features
52+
- [ ] Changed - for changes in existing functionality
53+
- [ ] Deprecated - for soon-to-be removed features
54+
- [ ] Removed - for now removed features
55+
- [ ] Fixed - for any bug fixes
56+
- [ ] Security - in case of vulnerabilities
57+
58+
#### Message
59+
60+
<!-- Add a changelog message here -->
61+
62+
</details>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: added
3+
4+
Development environment: allow contributors to specify a changelog entry directly from their Pull Request description.

.github/workflows/changelog.yml

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ on:
55
# (which are not included by default for the "pull_request" trigger).
66
# This is needed to allow skipping enforcement of the changelog in PRs with specific labels,
77
# as defined in the (optional) "skipLabels" property.
8-
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
8+
types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]
99

1010
jobs:
1111
# Enforces the addition of a changelog entry (a file in the .github/changelog directory) every pull request.
1212
changelog:
1313
runs-on: ubuntu-latest
1414
steps:
15-
- name: "Check changelog requirements"
15+
- name: "Check for Skip Changelog label"
16+
id: check-skip-label
1617
uses: actions/github-script@v7
1718
with:
1819
script: |
19-
const { repo: { owner, repo }, payload : { pull_request : { number, labels } } } = context;
20+
const { payload : { pull_request : { number, labels } } } = context;
2021
2122
// Check for Skip Changelog label
2223
core.debug( 'Changelog check: Check for Skip Changelog label' );
@@ -26,10 +27,21 @@ jobs:
2627
2728
if ( hasSkipLabel ) {
2829
core.info( `Skipping changelog requirement for this PR (#${ number }) because of the "${ skipLabel }" label.` );
29-
return;
30+
core.setOutput( 'skip-changelog', 'true' );
31+
} else {
32+
core.info( `No "${ skipLabel }" label found for PR #${ number }. Will check for changelog file.` );
33+
core.setOutput( 'skip-changelog', 'false' );
3034
}
3135
32-
// If no skip label, check for changelog file
36+
- name: "Check for changelog file"
37+
if: steps.check-skip-label.outputs.skip-changelog != 'true'
38+
id: check-changelog-file
39+
uses: actions/github-script@v7
40+
with:
41+
script: |
42+
const { repo: { owner, repo }, payload : { pull_request : { number } } } = context;
43+
44+
// Check for changelog file
3345
core.debug( `Changelog check: Get list of files modified in ${ owner }/${ repo } #${ number }.` );
3446
3547
const fileList = [];
@@ -58,7 +70,127 @@ jobs:
5870
5971
if ( hasChangelogFile ) {
6072
core.info( `PR #${ number } includes a changelog file.` );
73+
core.setOutput('has-changelog-file', 'true');
6174
} else {
6275
core.info( `PR #${ number } does not include a changelog file.` );
63-
core.setFailed( 'Your PR does not include a changelog file. Please add a changelog entry by running `composer changelog:add`, checking in the resulting file, and pushing that change to your branch.' );
76+
core.setOutput('has-changelog-file', 'false');
77+
}
78+
79+
- name: "Check for changelog information in PR body"
80+
id: check-pr-body
81+
if: steps.check-skip-label.outputs.skip-changelog != 'true' && steps.check-changelog-file.outputs.has-changelog-file != 'true'
82+
uses: actions/github-script@v7
83+
with:
84+
script: |
85+
const { repo: { owner, repo }, payload : { pull_request : { number, body } } } = context;
86+
87+
// Check if the PR body exists.
88+
if ( ! body ) {
89+
core.info( `PR #${ number } has no description.` );
90+
core.setFailed( 'Your PR does not include a changelog file and has no description to extract changelog information from. Please generate a changelog entry manually by running `composer changelog:add`.' );
91+
return;
92+
}
93+
94+
// Check if the "Automatically create a changelog entry" checkbox is checked.
95+
const autoCreateRegex = /-\s+\[x\]\s+Automatically\s+create\s+a\s+changelog\s+entry\s+from\s+the\s+details\s+below/i;
96+
const isAutoCreateChecked = autoCreateRegex.test( body );
97+
98+
if (! isAutoCreateChecked ) {
99+
core.info( `PR #${ number } does not have the "Automatically create a changelog entry" checkbox checked.` );
100+
core.setFailed( 'Your PR does not include a changelog file, and does not have the "Automatically create a changelog entry" checkbox checked. Please check the "Automatically create a changelog entry" checkbox and fill in all required information.' );
101+
return;
102+
}
103+
104+
core.info( `PR #${ number } has the "Automatically create a changelog entry" checkbox checked. Checking for changelog details...` );
105+
106+
// Extract all sections.
107+
const significanceSection = body.match(/#### Significance[\s\S]*?(?=####|$)/);
108+
const typeSection = body.match(/#### Type[\s\S]*?(?=####|$)/);
109+
const messageSection = body.match(/#### Message\s*\n+([\s\S]*?)(?=####|<\/details>|$)/);
110+
111+
// Bail early if any section is missing.
112+
if ( ! significanceSection || ! typeSection || ! messageSection ) {
113+
core.setFailed( 'Your PR is missing one or more required sections in the changelog details. Please generate a changelog entry manually by running `composer changelog:add`.' );
114+
return;
115+
}
116+
117+
// Process significance section.
118+
const significanceMatches = Array.from(
119+
significanceSection[0].matchAll( /-\s+\[x\]\s+(Patch|Minor|Major)/gi )
120+
);
121+
if ( significanceMatches.length !== 1 ) {
122+
core.setFailed( 'Your PR must have exactly one significance level checked (Patch, Minor, or Major) in the changelog details.' );
123+
return;
124+
}
125+
const significance = significanceMatches[0][1].toLowerCase();
126+
127+
// Process type section.
128+
const typeMatches = Array.from(
129+
typeSection[0].matchAll( /-\s+\[x\]\s+(Added|Changed|Deprecated|Removed|Fixed|Security)/gi )
130+
);
131+
if ( typeMatches.length !== 1 ) {
132+
core.setFailed( 'Your PR must have exactly one type checked (Added, Changed, Deprecated, Removed, Fixed, or Security) in the changelog details.' );
133+
return;
134+
}
135+
const type = typeMatches[0][1].toLowerCase();
136+
137+
// Process message section
138+
let changelogMessage = '';
139+
if ( messageSection ) {
140+
// Get the message and trim whitespace
141+
changelogMessage = messageSection[1].trim();
142+
143+
// If still empty after trimming, fail
144+
if ( ! changelogMessage ) {
145+
core.setFailed( 'Your PR has an empty changelog message. Please provide a meaningful message in the changelog details.' );
146+
return;
147+
}
148+
}
149+
150+
// All information is available, output it
151+
core.info( `Extracted changelog information - Significance: ${ significance }, Type: ${ type }, Message: ${ changelogMessage }` );
152+
core.setOutput( 'significance', significance );
153+
core.setOutput( 'type', type );
154+
core.setOutput( 'message', changelogMessage );
155+
core.setOutput( 'has-changelog-info', 'true' );
156+
157+
- name: "Create changelog file from PR description"
158+
id: create-changelog-file
159+
if: steps.check-skip-label.outputs.skip-changelog != 'true' && steps.check-changelog-file.outputs.has-changelog-file != 'true' && steps.check-pr-body.outputs.has-changelog-info == 'true'
160+
uses: actions/github-script@v7
161+
with:
162+
script: |
163+
const { repo: { owner, repo }, payload : { pull_request : { number, head : { ref } } } } = context;
164+
165+
// Get the changelog information from the previous step
166+
const significance = '${{ steps.check-pr-body.outputs.significance }}';
167+
const type = '${{ steps.check-pr-body.outputs.type }}';
168+
const message = '${{ steps.check-pr-body.outputs.message }}';
169+
170+
// Create the changelog file content
171+
const content = [
172+
`Significance: ${ significance }`,
173+
`Type: ${ type }`,
174+
'',
175+
message,
176+
''
177+
].join( '\n' );
178+
179+
const path = `.github/changelog/${ number }-from-description`;
180+
core.info( `Creating changelog file: ${ path }` );
181+
182+
try {
183+
// Create or update the file in the repository
184+
await github.rest.repos.createOrUpdateFileContents( {
185+
owner,
186+
repo,
187+
path,
188+
message: 'Add changelog',
189+
content: Buffer.from( content ).toString( 'base64' ),
190+
branch: ref
191+
} );
192+
193+
core.info( `Successfully created changelog file: ${ path }`);
194+
} catch ( error ) {
195+
core.setFailed( `Failed to create changelog file: ${ error.message }` );
64196
}

0 commit comments

Comments
 (0)