Skip to content

Commit 6fe48ba

Browse files
authored
chore(ci): housekeeping scripts & workflows related to PRs (#1023)
* chore: make lerna list explicit * chore: update codeowners to team * chore: updated CI scripts/workflow for merge events * fix: re-added commons * chore: update PR template * fix: repo name
1 parent 1a06fed commit 6fe48ba

14 files changed

+439
-95
lines changed

Diff for: .github/CODEOWNERS

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# These owners will be the default owners for everything in
2-
# the repo.
3-
# TODO: revisit this list
4-
* @saragerion, @alan-churley, @heitorlessa
1+
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
2+
3+
* @awslabs/aws-lambda-powertools-typescript

Diff for: .github/PULL_REQUEST_TEMPLATE.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
### Related issues, RFCs
2323

24-
<!--- Add here the link to one or more Github Issues or RFCs that are related to this PR. -->
25-
[#XXXXX](https://github.com/awslabs/aws-lambda-powertools-typescript/issues/XXXXX)
26-
[#ZZZZZ](https://github.com/awslabs/aws-lambda-powertools-typescript/issues/ZZZZZ)
24+
<!--- Add here the number to the Github Issue or RFC that is related to this PR. -->
25+
<!-- **Issue number:** #123 -->
26+
**Issue number:**
2727

2828
### PR status
2929

Diff for: .github/scripts/constants.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module.exports = Object.freeze({
2+
/** @type {string} */
3+
// Values: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
4+
"PR_ACTION": process.env.PR_ACTION?.replace(/"/g, '') || "",
5+
6+
/** @type {string} */
7+
"PR_AUTHOR": process.env.PR_AUTHOR?.replace(/"/g, '') || "",
8+
9+
/** @type {string} */
10+
"PR_BODY": process.env.PR_BODY || "",
11+
12+
/** @type {string} */
13+
"PR_TITLE": process.env.PR_TITLE || "",
14+
15+
/** @type {number} */
16+
"PR_NUMBER": process.env.PR_NUMBER || 0,
17+
18+
/** @type {string} */
19+
"PR_IS_MERGED": process.env.PR_IS_MERGED || "false",
20+
21+
/** @type {string} */
22+
"LABEL_BLOCK": "do-not-merge",
23+
24+
/** @type {string} */
25+
"LABEL_BLOCK_REASON": "need-issue",
26+
27+
/** @type {string} */
28+
"LABEL_PENDING_RELEASE": "pending-release",
29+
30+
/** @type {string} */
31+
"HANDLE_MAINTAINERS_TEAM": "@awslabs/aws-lambda-powertools-typescript",
32+
33+
/** @type {string[]} */
34+
"IGNORE_AUTHORS": ["dependabot[bot]"],
35+
36+
/** @type {string[]} */
37+
"AREAS": [
38+
"tracer",
39+
"metrics",
40+
"logger",
41+
],
42+
});

Diff for: .github/scripts/download_pr_artifact.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module.exports = async ({github, context, core}) => {
2+
const fs = require('fs');
3+
4+
const workflowRunId = process.env.WORKFLOW_ID;
5+
core.info(`Listing artifacts for workflow run ${workflowRunId}`);
6+
7+
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
8+
owner: context.repo.owner,
9+
repo: context.repo.repo,
10+
run_id: workflowRunId,
11+
});
12+
13+
const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0];
14+
15+
core.info(`Downloading artifacts for workflow run ${workflowRunId}`);
16+
const artifact = await github.rest.actions.downloadArtifact({
17+
owner: context.repo.owner,
18+
repo: context.repo.repo,
19+
artifact_id: matchArtifact.id,
20+
archive_format: 'zip',
21+
});
22+
23+
core.info("Saving artifact found", artifact);
24+
25+
fs.writeFileSync('pr.zip', Buffer.from(artifact.data));
26+
}

Diff for: .github/scripts/label_missing_related_issue.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const {
2+
PR_ACTION,
3+
PR_AUTHOR,
4+
PR_BODY,
5+
PR_NUMBER,
6+
IGNORE_AUTHORS,
7+
LABEL_BLOCK,
8+
LABEL_BLOCK_REASON
9+
} = require("./constants");
10+
11+
module.exports = async ({github, context, core}) => {
12+
if (IGNORE_AUTHORS.includes(PR_AUTHOR)) {
13+
return core.notice("Author in IGNORE_AUTHORS list; skipping...");
14+
}
15+
16+
if (PR_ACTION != "opened") {
17+
return core.notice("Only newly open PRs are labelled to avoid spam; skipping");
18+
}
19+
20+
const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?<issue>\d+)/;
21+
const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY);
22+
if (isMatch == null) {
23+
core.info(`No related issue found, maybe the author didn't use the template but there is one.`);
24+
25+
let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure.";
26+
await github.rest.issues.createComment({
27+
owner: context.repo.owner,
28+
repo: context.repo.repo,
29+
body: msg,
30+
issue_number: PR_NUMBER,
31+
});
32+
33+
return await github.rest.issues.addLabels({
34+
issue_number: PR_NUMBER,
35+
owner: context.repo.owner,
36+
repo: context.repo.repo,
37+
labels: [LABEL_BLOCK, LABEL_BLOCK_REASON]
38+
});
39+
}
40+
}

Diff for: .github/scripts/label_pr_based_on_title.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const { PR_NUMBER, PR_TITLE, AREAS } = require("./constants");
2+
3+
module.exports = async ({github, context, core}) => {
4+
const FEAT_REGEX = /feat(\((.+)\))?(:.+)/
5+
const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/
6+
const DOCS_REGEX = /(docs|doc)(\((.+)\))?(:.+)/
7+
const CHORE_REGEX = /(chore)(\((.+)\))?(:.+)/
8+
const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/
9+
const REFACTOR_REGEX = /(refactor)(\((.+)\))?(:.+)/
10+
11+
const labels = {
12+
"feature": FEAT_REGEX,
13+
"bug": BUG_REGEX,
14+
"documentation": DOCS_REGEX,
15+
"internal": CHORE_REGEX,
16+
"enhancement": REFACTOR_REGEX,
17+
"deprecated": DEPRECATED_REGEX,
18+
};
19+
20+
// Maintenance: We should keep track of modified PRs in case their titles change
21+
let miss = 0;
22+
try {
23+
for (const label in labels) {
24+
const matcher = new RegExp(labels[label]);
25+
const matches = matcher.exec(PR_TITLE);
26+
if (matches != null) {
27+
core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`);
28+
29+
await github.rest.issues.addLabels({
30+
issue_number: PR_NUMBER,
31+
owner: context.repo.owner,
32+
repo: context.repo.repo,
33+
labels: [label]
34+
});
35+
36+
const area = matches[2]; // second capture group contains the area
37+
if (AREAS.indexOf(area) > -1) {
38+
core.info(`Auto-labeling PR ${PR_NUMBER} with area ${area}`);
39+
await github.rest.issues.addLabels({
40+
issue_number: PR_NUMBER,
41+
owner: context.repo.owner,
42+
repo: context.repo.repo,
43+
labels: [`area/${area}`],
44+
});
45+
} else {
46+
core.debug(`'${PR_TITLE}' didn't match any known area.`);
47+
}
48+
49+
return;
50+
} else {
51+
core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`);
52+
miss += 1;
53+
}
54+
}
55+
} finally {
56+
if (miss == Object.keys(labels).length) {
57+
core.notice(`PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...`);
58+
}
59+
}
60+
}

Diff for: .github/scripts/label_related_issue.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const {
2+
PR_AUTHOR,
3+
PR_BODY,
4+
PR_NUMBER,
5+
IGNORE_AUTHORS,
6+
LABEL_PENDING_RELEASE,
7+
HANDLE_MAINTAINERS_TEAM,
8+
PR_IS_MERGED,
9+
} = require("./constants")
10+
11+
module.exports = async ({github, context, core}) => {
12+
if (IGNORE_AUTHORS.includes(PR_AUTHOR)) {
13+
return core.notice("Author in IGNORE_AUTHORS list; skipping...");
14+
}
15+
16+
if (PR_IS_MERGED == "false") {
17+
return core.notice("Only merged PRs to avoid spam; skipping");
18+
}
19+
20+
const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?<issue>\d+)/;
21+
22+
const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY);
23+
24+
try {
25+
if (!isMatch) {
26+
core.setFailed(`Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}`);
27+
return await github.rest.issues.createComment({
28+
owner: context.repo.owner,
29+
repo: context.repo.repo,
30+
body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`,
31+
issue_number: PR_NUMBER,
32+
});
33+
}
34+
} catch (error) {
35+
core.setFailed(`Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}`);
36+
throw new Error(error);
37+
}
38+
39+
const { groups: {issue} } = isMatch;
40+
41+
try {
42+
core.info(`Auto-labeling related issue ${issue} for release`)
43+
await github.rest.issues.addLabels({
44+
issue_number: issue,
45+
owner: context.repo.owner,
46+
repo: context.repo.repo,
47+
labels: [LABEL_PENDING_RELEASE]
48+
});
49+
} catch (error) {
50+
core.setFailed(`Is this issue number (${issue}) valid? Perhaps a discussion?`);
51+
throw new Error(error);
52+
}
53+
54+
const { groups: {relatedIssueNumber} } = isMatch;
55+
56+
core.info(`Auto-labeling related issue ${relatedIssueNumber} for release`);
57+
return await github.rest.issues.addLabels({
58+
issue_number: relatedIssueNumber,
59+
owner: context.repo.owner,
60+
repo: context.repo.repo,
61+
labels: [relatedIssueNumber]
62+
});
63+
}

Diff for: .github/scripts/save_pr_details.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = async ({context, core}) => {
2+
const fs = require('fs');
3+
const filename = "pr.txt";
4+
5+
try {
6+
fs.writeFileSync(`./${filename}`, JSON.stringify(context.payload));
7+
8+
return `PR successfully saved ${filename}`;
9+
} catch (err) {
10+
core.setFailed("Failed to save PR details");
11+
console.error(err);
12+
}
13+
}

Diff for: .github/workflows/label_pr_on_title.yml

+20-73
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,38 @@
11
name: Label PR based on title
22

3-
# pull_request_target event sends an admin GH token to forks
4-
# this however depends on another workflow so it all runs within the base repo safely
5-
# "Record PR number" workflow safely captures PR title and number
6-
# This workflow uses this information to label the PR based on its semantic title
73
on:
84
workflow_run:
9-
workflows: ["Record PR number"]
5+
workflows: ["Record PR details"]
106
types:
117
- completed
128

139
jobs:
14-
label_pr:
15-
runs-on: ubuntu-latest
10+
get_pr_details:
1611
# Guardrails to only ever run if PR recording workflow was indeed
1712
# run in a PR event and ran successfully
18-
if: >
19-
${{ github.event.workflow_run.event == 'pull_request' &&
20-
github.event.workflow_run.conclusion == 'success' }}
13+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
14+
uses: ./.github/workflows/reusable_export_pr_details.yml
15+
with:
16+
record_pr_workflow_id: ${{ github.event.workflow_run.id }}
17+
workflow_origin: ${{ github.event.repository.full_name }}
18+
secrets:
19+
token: ${{ secrets.GITHUB_TOKEN }}
20+
label_pr:
21+
needs: get_pr_details
22+
runs-on: ubuntu-latest
2123
steps:
22-
- name: 'Download artifact'
23-
uses: actions/github-script@v6
24-
# For security, we only download artifacts tied to the successful PR recording workflow
25-
with:
26-
script: |
27-
const fs = require('fs');
28-
29-
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
30-
owner: context.repo.owner,
31-
repo: context.repo.repo,
32-
run_id: ${{github.event.workflow_run.id }},
33-
});
34-
35-
const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0];
36-
37-
const artifact = await github.rest.actions.downloadArtifact({
38-
owner: context.repo.owner,
39-
repo: context.repo.repo,
40-
artifact_id: matchArtifact.id,
41-
archive_format: 'zip',
42-
});
43-
44-
fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(artifact.data));
45-
# NodeJS standard library doesn't provide ZIP capabilities; use system `unzip` command instead
46-
- run: unzip pr.zip
47-
48-
- name: 'Label PR based on title'
24+
- name: Checkout repository
25+
uses: actions/checkout@v3
26+
- name: "Label PR based on title"
4927
uses: actions/github-script@v6
28+
env:
29+
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
30+
PR_TITLE: ${{ needs.get_pr_details.outputs.prTitle }}
5031
with:
5132
github-token: ${{ secrets.GITHUB_TOKEN }}
5233
# This safely runs in our base repo, not on fork
5334
# thus allowing us to provide a write access token to label based on PR title
5435
# and label PR based on semantic title accordingly
5536
script: |
56-
const fs = require('fs');
57-
const pr_number = Number(fs.readFileSync('./number'));
58-
const pr_title = fs.readFileSync('./title', 'utf-8').trim();
59-
60-
const FEAT_REGEX = /feat(\((\w+)\))?(\:.+)/
61-
const BUG_REGEX = /(fix|bug)(\((\w+)\))?(\:.+)/
62-
const DOCS_REGEX = /(docs|doc)(\((\w+)\))?(\:.+)/
63-
const CHORE_REGEX = /(chore)(\((\w+)\))?(\:.+)/
64-
const DEPRECATED_REGEX = /(deprecated)(\((\w+)\))?(\:.+)/
65-
const REFACTOR_REGEX = /(refactor)(\((\w+)\))?(\:.+)/
66-
67-
const labels = {
68-
"feature": FEAT_REGEX,
69-
"bug": BUG_REGEX,
70-
"documentation": DOCS_REGEX,
71-
"internal": CHORE_REGEX,
72-
"enhancement": REFACTOR_REGEX,
73-
"deprecated": DEPRECATED_REGEX,
74-
}
75-
76-
for (const label in labels) {
77-
const matcher = new RegExp(labels[label])
78-
const isMatch = matcher.exec(pr_title)
79-
if (isMatch != null) {
80-
console.info(`Auto-labeling PR ${pr_number} with ${label}`)
81-
82-
await github.rest.issues.addLabels({
83-
issue_number: pr_number,
84-
owner: context.repo.owner,
85-
repo: context.repo.repo,
86-
labels: [label]
87-
})
88-
89-
break
90-
}
91-
}
37+
const script = require('.github/scripts/label_pr_based_on_title.js')
38+
await script({github, context, core})

0 commit comments

Comments
 (0)