Skip to content

Commit 52cf1f2

Browse files
lidavidmkou
andauthored
GH-499: Require PR labels for changelog (#594)
- Validate that at least one of certain labels are present on a PR. - Validate the format of the title. - Add label for things that are not dependencies but are other chores (release/build work). - Add label for documentation updates. - Validate that a PR titled `GH-499` is actually linked to issue `#499` by poking the GitHub API. - Add the GitHub config to use these labels for changelog generation. Closes #499. --------- Co-authored-by: Sutou Kouhei <[email protected]>
1 parent f1fd621 commit 52cf1f2

File tree

6 files changed

+390
-2
lines changed

6 files changed

+390
-2
lines changed

.github/pull_request_template.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## What's Changed
2+
3+
Please fill in a description of the changes here.
4+
5+
**This contains breaking changes.** <!-- Remove this line if there are no breaking changes. -->
6+
7+
Closes #NNN.

.github/release.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
changelog:
19+
categories:
20+
- title: Breaking Changes
21+
labels:
22+
- breaking-change
23+
24+
- title: New Features and Enhancements
25+
labels:
26+
- enhancement
27+
28+
- title: Bug Fixes
29+
labels:
30+
- bug-fix
31+
32+
- title: Other Changes
33+
labels:
34+
- "*"

.github/workflows/dev_pr.js

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
async function have_comment(github, context, pr_number, tag) {
19+
console.log(`Looking for existing comment on ${pr_number} with substring ${tag}`);
20+
const query = `
21+
query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
22+
repository(owner: $owner, name: $name) {
23+
pullRequest(number: $number) {
24+
id
25+
comments (after:$cursor, first: 50) {
26+
nodes {
27+
id
28+
body
29+
author {
30+
login
31+
}
32+
}
33+
pageInfo {
34+
endCursor
35+
hasNextPage
36+
}
37+
}
38+
}
39+
}
40+
}`;
41+
const tag_regexp = new RegExp(tag);
42+
43+
let cursor = null;
44+
let pr_id = null;
45+
while (true) {
46+
const result = await github.graphql(query, {
47+
owner: context.repo.owner,
48+
name: context.repo.repo,
49+
number: pr_number,
50+
cursor,
51+
});
52+
pr_id = result.repository.pullRequest.id;
53+
cursor = result.repository.pullRequest.comments.pageInfo;
54+
const comments = result.repository.pullRequest.comments.nodes;
55+
56+
for (const comment of comments) {
57+
console.log(comment);
58+
if (comment.author.login === "github-actions" &&
59+
comment.body.match(tag_regexp) !== null) {
60+
console.log("Found existing comment");
61+
return {pr_id, comment_id: comment.id};
62+
}
63+
}
64+
65+
if (!result.repository.pullRequest.comments.hasNextPage ||
66+
comments.length === 0) {
67+
break;
68+
}
69+
}
70+
console.log("No existing comment");
71+
return {pr_id, comment_id: null};
72+
}
73+
74+
async function upsert_comment(github, {pr_id, comment_id}, body, visible) {
75+
console.log(`Upsert comment (pr_id=${pr_id}, comment_id=${comment_id}, visible=${visible})`);
76+
if (!visible) {
77+
if (comment_id === null) return;
78+
79+
const query = `
80+
mutation makeComment($comment: ID!) {
81+
minimizeComment(input: {subjectId: $comment, classifier: RESOLVED}) {
82+
clientMutationId
83+
}
84+
}`;
85+
await github.graphql(query, {
86+
comment: comment_id,
87+
body,
88+
});
89+
return;
90+
}
91+
92+
if (comment_id === null) {
93+
const query = `
94+
mutation makeComment($pr: ID!, $body: String!) {
95+
addComment(input: {subjectId: $pr, body: $body}) {
96+
clientMutationId
97+
}
98+
}`;
99+
await github.graphql(query, {
100+
pr: pr_id,
101+
body,
102+
});
103+
} else {
104+
const query = `
105+
mutation makeComment($comment: ID!, $body: String!) {
106+
unminimizeComment(input: {subjectId: $comment}) {
107+
clientMutationId
108+
}
109+
updateIssueComment(input: {id: $comment, body: $body}) {
110+
clientMutationId
111+
}
112+
}`;
113+
await github.graphql(query, {
114+
comment: comment_id,
115+
body,
116+
});
117+
}
118+
}
119+
120+
module.exports = {
121+
check_title_format: function({core, github, context}) {
122+
const title = context.payload.pull_request.title;
123+
if (title.startsWith("MINOR: ")) {
124+
context.log("PR is a minor PR");
125+
return {"issue": null};
126+
}
127+
128+
const match = title.match(/^GH-([0-9]+): .*$/);
129+
if (match === null) {
130+
core.setFailed("Invalid PR title format. Must either be MINOR: or GH-NNN:");
131+
return {"issue": null};
132+
}
133+
return {"issue": parseInt(match[1], 10)};
134+
},
135+
136+
apply_labels: async function({core, github, context}) {
137+
const body = (context.payload.pull_request.body || "").split(/\n/g);
138+
var has_breaking = false;
139+
for (const line of body) {
140+
if (line.trim().startsWith("**This contains breaking changes.**")) {
141+
has_breaking = true;
142+
break;
143+
}
144+
}
145+
if (has_breaking) {
146+
console.log("PR has breaking changes");
147+
await github.rest.issues.addLabels({
148+
issue_number: context.payload.pull_request.number,
149+
owner: context.repo.owner,
150+
repo: context.repo.repo,
151+
labels: ["breaking-change"],
152+
});
153+
} else {
154+
console.log("PR has no breaking changes");
155+
}
156+
},
157+
158+
check_labels: async function({core, github, context}) {
159+
const categories = ["bug-fix", "chore", "dependencies", "documentation", "enhancement"];
160+
const labels = (context.payload.pull_request.labels || []);
161+
const required = new Set(categories);
162+
var found = false;
163+
164+
for (const label of labels) {
165+
console.log(`Found label ${label.name}`);
166+
if (required.has(label.name)) {
167+
found = true;
168+
break;
169+
}
170+
}
171+
172+
// Look to see if we left a comment before.
173+
const comment_tag = "label_helper_comment";
174+
const maybe_comment_id = await have_comment(github, context, context.payload.pull_request.number, comment_tag);
175+
console.log("Found comment?");
176+
console.log(maybe_comment_id);
177+
const body_text = `
178+
<!-- ${comment_tag} -->
179+
Thank you for opening a pull request!
180+
181+
Please label the PR with one or more of:
182+
183+
${categories.map(c => `- ${c}`).join("\n")}
184+
185+
Also, add the 'breaking-change' label if appropriate.
186+
187+
See [CONTRIBUTING.md](https://github.com/apache/arrow-java/blob/main/CONTRIBUTING.md) for details.
188+
`;
189+
190+
if (found) {
191+
console.log("PR has appropriate label(s)");
192+
await upsert_comment(github, maybe_comment_id, body_text, false);
193+
} else {
194+
console.log(body_text);
195+
await upsert_comment(github, maybe_comment_id, body_text, true);
196+
core.setFailed("Missing required labels. See CONTRIBUTING.md");
197+
}
198+
},
199+
200+
check_linked_issue: async function({core, github, context, issue}) {
201+
console.log(issue);
202+
if (issue.issue === null) {
203+
console.log("This is a MINOR PR");
204+
return;
205+
}
206+
const expected = `https://github.com/apache/arrow-java/issues/${issue.issue}`;
207+
208+
const query = `
209+
query($owner: String!, $name: String!, $number: Int!) {
210+
repository(owner: $owner, name: $name) {
211+
pullRequest(number: $number) {
212+
closingIssuesReferences(first: 50) {
213+
edges {
214+
node {
215+
number
216+
}
217+
}
218+
}
219+
}
220+
}
221+
}`;
222+
223+
const result = await github.graphql(query, {
224+
owner: context.repo.owner,
225+
name: context.repo.repo,
226+
number: context.payload.pull_request.number,
227+
});
228+
const issues = result.repository.pullRequest.closingIssuesReferences.edges;
229+
console.log(issues);
230+
231+
for (const link of issues) {
232+
console.log(`PR is linked to ${link.node.number}`);
233+
if (link.node.number === issue.issue) {
234+
console.log(`Found link to ${expected}`);
235+
return;
236+
}
237+
}
238+
console.log(`Did not find link to ${expected}`);
239+
core.setFailed("Missing link to issue in title");
240+
},
241+
};

.github/workflows/dev_pr.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
name: Dev PR
19+
20+
on:
21+
pull_request_target:
22+
types:
23+
- labeled
24+
- unlabeled
25+
- opened
26+
- edited
27+
- reopened
28+
- synchronize
29+
- ready_for_review
30+
- review_requested
31+
32+
concurrency:
33+
group: ${{ github.repository }}-${{ github.ref }}-${{ github.workflow }}
34+
cancel-in-progress: true
35+
36+
permissions:
37+
contents: read
38+
pull-requests: write
39+
40+
jobs:
41+
pr-label:
42+
name: "Ensure PR format"
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
46+
with:
47+
fetch-depth: 0
48+
persist-credentials: false
49+
50+
- name: Ensure PR title format
51+
id: title-format
52+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
53+
with:
54+
script: |
55+
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
56+
return scripts.check_title_format({core, github, context});
57+
58+
- name: Label PR
59+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
60+
with:
61+
script: |
62+
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
63+
await scripts.apply_labels({core, github, context});
64+
65+
- name: Ensure PR is labeled
66+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
67+
with:
68+
script: |
69+
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
70+
await scripts.check_labels({core, github, context});
71+
72+
- name: Ensure PR is linked to an issue
73+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
74+
with:
75+
script: |
76+
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
77+
await scripts.check_linked_issue({core, github, context, issue: ${{ steps.title-format.outputs.result }}});

0 commit comments

Comments
 (0)