Skip to content
Snippets Groups Projects
Unverified Commit 25c4cb0b authored by Nemanja Glumac's avatar Nemanja Glumac Committed by GitHub
Browse files

Fix milestone reminder (#42861)

* Add a helper to extract linked issues from PR body

* Add tests

* Tweak the workflow

* Fix the workflow

* Delete obsolete code

* Improve the message

* Fix whitespace error
parent f54e3d5a
Branches
Tags
No related merge requests found
......@@ -16,21 +16,20 @@ jobs:
pull-requests: write
issues: read
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- uses: actions/github-script@v7
with:
script: | # js
const { getLinkedIssues } = require('${{ github.workspace }}/release/dist/index.cjs');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pullNumber = '${{ github.event.pull_request.number }}';
// the github API doesn't expose linked issues, so we have to parse the body ourselves
function getLinkedIssueId(pull) {
// https://regexr.com/7sk94
const match = pull.data.body.match(/(close(s|d)?|fixe?(s|d)?|resolve(s|d)?) (#|https?:\/\/github.com\/.+\/issues\/)(\d+)/i);
return match && match?.[2];
}
function getMilestone(pullOrIssue) {
// pull has pull.data.milestone, issue has issue.milestone
return pullOrIssue?.data?.milestone || pullOrIssue?.milestone;
......@@ -47,34 +46,41 @@ jobs:
process.exit(0);
}
const issueId = getLinkedIssueId(pull);
// the github API doesn't expose linked issues, so we have to parse the body ourselves
const prDescription = pull.data.body;
const linkedIssues = getLinkedIssues(prDescription);
if (issueId) {
console.log('linked issue found', issueId);
if (Array.isArray(linkedIssues) && linkedIssues.length > 0) {
console.log('linked issue(s) found');
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueId,
});
linkedIssues.forEach(async (issueId, index) => {
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueId,
});
const milestone = getMilestone(issue);
const milestone = getMilestone(issue);
if (milestone) {
console.log("Linked issue has milestone", milestone.title);
process.exit(0);
}
}
if (milestone) {
console.log(`Linked issue #${issueId} has milestone`, milestone.title);
index === linkedIssues.length -1 && process.exit(0);
} else {
console.log("No milestone found");
console.log("No milestone found");
const author = pull.data.user.login;
const guideLink = "https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#3dea255ffa3b4f74942a227844e889fa";
const message = `@${author} Did you forget to add a milestone to the issue #${issueId} linked in this PR? _[When and where should I add a milestone?](${guideLink})_`;
const author = pull.data.user.login;
const guideLink = "https://www.notion.so/metabase/Metabase-Branching-Strategy-6eb577d5f61142aa960a626d6bbdfeb3?pvs=4#3dea255ffa3b4f74942a227844e889fa";
const message = `@${author} Did you forget to add a milestone to the issue for this PR? _[When and where should I add a milestone?](${guideLink})_`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: message,
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body: message,
});
// Exit as soon as we found an issue without a milestone and alerted the author.
process.exit(0);
}
});
}
export * from "./backports";
export * from "./github";
export * from "./linked-issues";
export * from "./release-notes";
export * from "./release-status";
export * from "./slack";
......
export function getLinkedIssues(body: string) {
const matches = body.match(
/(close(s|d)?|fixe?(s|d)?|resolve(s|d)?) (#|https?:\/\/github.com\/.+\/issues\/)(\d+)/gi,
);
if (matches) {
return matches.map(m => {
const numberMatch = m.match(/\d+/);
return numberMatch ? numberMatch[0] : null;
});
}
return null;
}
import { getLinkedIssues } from "./linked-issues";
const closingKeywords = [
"Close",
"Closes",
"Closed",
"Fix",
"Fixes",
"Fixed",
"Resolve",
"Resolves",
"Resolved",
];
const issueUrl = (id: number | string) =>
`https://github.com/metabase/metabase/issues/${id}`;
describe("getLinkedIssues", () => {
describe("null", () => {
it("should return `null` when body is empty", () => {
expect(getLinkedIssues("")).toBeNull();
expect(getLinkedIssues(" ")).toBeNull();
});
it("should return `null` when body doesn't contain the closing keyword", () => {
expect(getLinkedIssues("#123")).toBeNull();
expect(getLinkedIssues("Related to #123")).toBeNull();
expect(getLinkedIssues(`Reproduces ${issueUrl(123)}`)).toBeNull();
expect(getLinkedIssues(issueUrl(123))).toBeNull();
});
it("should return `null` when body doesn't contain the issue", () => {
expect(getLinkedIssues("Lorem ipsum dolor sit amet.")).toBeNull();
expect(getLinkedIssues("Fix 123.")).toBeNull();
expect(getLinkedIssues("Fix#123.")).toBeNull();
expect(getLinkedIssues("Fix # 123.")).toBeNull();
expect(getLinkedIssues("123 456")).toBeNull();
expect(
getLinkedIssues("Close https://github.com/metabase/metabase/pull/123."),
).toBeNull();
});
it("should return `null` when the issue doesn't immediatelly follow the closing keyword", () => {
// Two or more spaces
expect(getLinkedIssues("Fix #123")).toBeNull();
// Newline
expect(
getLinkedIssues(`
Fix
#123
`),
).toBeNull();
});
});
describe.each(closingKeywords)("smoke tests", closingKeyword => {
describe("shorthand syntax", () => {
it(`should return the issue id for ${closingKeyword}`, () => {
expect(getLinkedIssues(`${closingKeyword} #123`)).toEqual(["123"]);
});
it(`should return the issue id for ${closingKeyword.toUpperCase()}`, () => {
expect(getLinkedIssues(`${closingKeyword.toUpperCase()} #123`)).toEqual(
["123"],
);
});
it(`should return the issue id for ${closingKeyword.toLowerCase()}`, () => {
expect(getLinkedIssues(`${closingKeyword.toLowerCase()} #123`)).toEqual(
["123"],
);
});
});
describe("https syntax", () => {
const id = 123;
const url = issueUrl(id);
it(`should return the issue id for ${closingKeyword}`, () => {
expect(getLinkedIssues(`${closingKeyword} ${url}`)).toEqual([`${id}`]);
});
it(`should return the issue id for ${closingKeyword.toUpperCase()}`, () => {
expect(
getLinkedIssues(`${closingKeyword.toUpperCase()} ${url}`),
).toEqual([`${id}`]);
});
it(`should return the issue id for ${closingKeyword.toLowerCase()}`, () => {
expect(
getLinkedIssues(`${closingKeyword.toLowerCase()} ${url}`),
).toEqual([`${id}`]);
});
});
});
describe("multiple issues", () => {
const body = `
Fix #123.
Closes ${issueUrl(456)}, and resolves #789.
On top of that, reproduces #888!
`;
it("should return the issue ids", () => {
expect(getLinkedIssues(body)).toEqual(["123", "456", "789"]);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment