-
Notifications
You must be signed in to change notification settings - Fork 86
278 lines (240 loc) · 12.9 KB
/
changelog.yml
File metadata and controls
278 lines (240 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
name: 'Changelog entry'
on:
pull_request_target:
# The specific activity types are listed here to include "labeled" and "unlabeled"
# (which are not included by default for the "pull_request" trigger).
# This is needed to allow skipping enforcement of the changelog in PRs with specific labels,
# as defined in the (optional) "skipLabels" property.
types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
# Enforces the addition of a changelog entry (a file in the .github/changelog directory) every pull request.
changelog:
runs-on: ubuntu-latest
steps:
- name: 'Check for Skip Changelog label, or if the PR is a draft'
id: check-skip-label
uses: actions/github-script@v8
with:
script: |
const { payload : { pull_request : { number, labels, draft = false, base : { ref } } } } = context;
// Check for Skip Changelog label
core.debug( 'Changelog check: Check for Skip Changelog label' );
const skipLabel = 'Skip Changelog';
const prLabels = labels.map( label => label.name );
const hasSkipLabel = prLabels.includes( skipLabel );
if ( hasSkipLabel ) {
core.info( `Skipping changelog requirement for this PR (#${ number }) because of the "${ skipLabel }" label.` );
core.setOutput( 'skip-changelog', 'true' );
} else if ( draft ) {
core.info( `Skipping changelog requirement for this PR (#${ number }) because it is a draft.` );
core.setOutput( 'skip-changelog', 'true' );
} else if ( ref !== 'trunk' ) {
core.info( `Skipping changelog requirement for this PR (#${ number }) because it is not against the "trunk" branch.` );
core.setOutput( 'skip-changelog', 'true' );
} else {
core.info( `No "${ skipLabel }" label found for PR #${ number }. Will check for changelog file.` );
core.setOutput( 'skip-changelog', 'false' );
}
- name: 'Check for changelog file'
if: steps.check-skip-label.outputs.skip-changelog != 'true'
id: check-changelog-file
uses: actions/github-script@v8
with:
script: |
const { repo: { owner, repo }, payload : { pull_request : { number } } } = context;
// Check for changelog file
core.debug( `Changelog check: Get list of files modified in ${ owner }/${ repo } #${ number }.` );
const fileList = [];
for await ( const response of github.paginate.iterator( github.rest.pulls.listFiles, {
owner,
repo,
pull_number: +number,
per_page: 100,
} ) ) {
for ( const file of response.data ) {
fileList.push( file.filename );
if ( file.previous_filename ) {
fileList.push( file.previous_filename );
}
}
}
const hasChangelogFile = fileList.some( file => {
core.debug( `Checking file: ${ file }` );
if ( file.startsWith( '.github/changelog' ) ) {
core.info( `Found changelog file: ${ file }` );
return true;
}
return false;
} );
if ( hasChangelogFile ) {
core.info( `PR #${ number } includes a changelog file.` );
core.setOutput('has-changelog-file', 'true');
} else {
core.info( `PR #${ number } does not include a changelog file.` );
core.setOutput('has-changelog-file', 'false');
}
- name: 'Check for changelog information in PR body'
id: check-pr-body
if: steps.check-skip-label.outputs.skip-changelog != 'true' && steps.check-changelog-file.outputs.has-changelog-file != 'true'
uses: actions/github-script@v8
with:
script: |
const { repo: { owner, repo }, payload : { pull_request : { number, body } } } = context;
// Check if the PR body exists.
if ( ! body ) {
core.info( `PR #${ number } has no description.` );
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`.' );
return;
}
// Check if the "Automatically create a changelog entry" checkbox is checked.
const autoCreateRegex = /-\s+\[x\]\s+Automatically\s+create\s+a\s+changelog\s+entry\s+from\s+the\s+details\s+below/i;
const isAutoCreateChecked = autoCreateRegex.test( body );
if (! isAutoCreateChecked ) {
core.info( `PR #${ number } does not have the "Automatically create a changelog entry" checkbox checked.` );
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.' );
return;
}
core.info( `PR #${ number } has the "Automatically create a changelog entry" checkbox checked. Checking for changelog details...` );
// Extract all sections.
const significanceSection = body.match(/#### Significance[\s\S]*?(?=####|$)/);
const typeSection = body.match(/#### Type[\s\S]*?(?=####|$)/);
const messageSection = body.match(/#### Message\s*\n+([\s\S]*?)(?=####|<\/details>|$)/);
// Bail early if any section is missing.
if ( ! significanceSection || ! typeSection || ! messageSection ) {
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`.' );
return;
}
// Process significance section.
const significanceMatches = Array.from(
significanceSection[0].matchAll( /-\s+\[x\]\s+(Patch|Minor|Major)/gi )
);
if ( significanceMatches.length !== 1 ) {
core.setFailed( 'Your PR must have exactly one significance level checked (Patch, Minor, or Major) in the changelog details.' );
return;
}
const significance = significanceMatches[0][1].toLowerCase();
// Process type section.
const typeMatches = Array.from(
typeSection[0].matchAll( /-\s+\[x\]\s+(Added|Changed|Deprecated|Removed|Fixed|Security)/gi )
);
if ( typeMatches.length !== 1 ) {
core.setFailed( 'Your PR must have exactly one type checked (Added, Changed, Deprecated, Removed, Fixed, or Security) in the changelog details.' );
return;
}
const type = typeMatches[0][1].toLowerCase();
// Process message section
let changelogMessage = '';
if ( messageSection ) {
// Get the message and trim whitespace
changelogMessage = messageSection[1].trim();
// If still empty after trimming, fail
if ( ! changelogMessage ) {
core.setFailed( 'Your PR has an empty changelog message. Please provide a meaningful message in the changelog details.' );
return;
}
}
// All information is available, output it
core.info( `Extracted changelog information - Significance: ${ significance }, Type: ${ type }, Message: ${ changelogMessage }` );
core.setOutput( 'significance', significance );
core.setOutput( 'type', type );
core.setOutput( 'message', changelogMessage );
core.setOutput( 'has-changelog-info', 'true' );
- name: 'Create changelog file from PR description'
id: create-changelog-file
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'
env:
PR_MESSAGE: '${{ steps.check-pr-body.outputs.message }}'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.API_TOKEN_GITHUB }}
script: |
const { repo: { owner, repo }, payload : { pull_request : { number, head : { ref, repo: head_repo } } } } = context;
// Get the changelog information from the previous step.
const significance = '${{ steps.check-pr-body.outputs.significance }}';
const type = '${{ steps.check-pr-body.outputs.type }}';
// Get the raw message from the previous step and process it.
// We need to handle it as a regular string to avoid template literal syntax issues.
// The message is passed through environment variables to avoid syntax issues.
const rawMessage = process.env.PR_MESSAGE;
// Ensure the message is not empty.
if ( ! rawMessage ) {
core.setFailed( 'Changelog message is missing or empty. Please provide a meaningful message in the PR description.' );
return;
}
// Process the message to remove HTML comments and preserve special characters.
let message = rawMessage
// Remove HTML comments.
.replace( /<!--[\s\S]*?-->/g, '' )
// Trim whitespace.
.trim();
// Create the changelog file content.
const content = [
`Significance: ${ significance }`,
`Type: ${ type }`,
'',
message,
''
].join( '\n' );
const path = `.github/changelog/${ number }-from-description`;
core.info( `Creating changelog file: ${ path }` );
// Check if PR is from a fork
const isFromFork = head_repo.full_name !== `${owner}/${repo}`;
if ( isFromFork ) {
core.info( 'PR is from a fork, so we cannot create the changelog file automatically.' );
// For forks, we cannot create the file automatically, so we add a helpful comment to the PR,
// asking the contributor to create the file manually.
const commentBody = `👋 Thanks for your contribution!
I see you have provided all the required changelog information in your PR description. To complete the process, you'll need to create a changelog file in your branch.
Please create a file named \`.github/changelog/${number}-from-description\` with this content:
\`\`\`
${content}
\`\`\`
You can do this by either:
1. Running \`composer changelog:add\` in your local repository, or
2. Creating the file manually with the content above
Once you've committed and pushed the file to your PR branch, the checks will pass automatically.`;
// Check for existing comments first
let hasExistingComment = false;
for await ( const response of github.paginate.iterator( github.rest.issues.listComments, {
owner,
repo,
issue_number: +number,
per_page: 100,
} ) ) {
for ( const comment of response.data ) {
if ( comment.body.includes('👋 Thanks for your contribution!') ) {
hasExistingComment = true;
break;
}
}
if ( hasExistingComment ) {
core.info( 'A comment already exists, so we will not add another one.' );
break;
}
}
// Only post the comment if we haven't posted one before
if ( ! hasExistingComment ) {
core.info( 'No existing comment found, so we will add a new one.' );
await github.rest.issues.createComment( {
owner,
repo,
issue_number: number,
body: commentBody
} );
}
return;
}
try {
// For internal PRs, create the file automatically
await github.rest.repos.createOrUpdateFileContents( {
owner,
repo,
path,
message: 'Add changelog',
content: Buffer.from( content ).toString( 'base64' ),
branch: ref
} );
core.info( `Successfully created changelog file: ${ path }`);
} catch ( error ) {
core.setFailed( `Failed to create changelog file: ${ error.message }` );
}