Skip to content

Commit

Permalink
chore(ci): housekeeping scripts & workflows related to PRs (#1023)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dreamorosi authored Aug 3, 2022
1 parent 1a06fed commit 6fe48ba
Show file tree
Hide file tree
Showing 14 changed files with 439 additions and 95 deletions.
7 changes: 3 additions & 4 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# These owners will be the default owners for everything in
# the repo.
# TODO: revisit this list
* @saragerion, @alan-churley, @heitorlessa
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

* @awslabs/aws-lambda-powertools-typescript
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@

### Related issues, RFCs

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

### PR status

Expand Down
42 changes: 42 additions & 0 deletions .github/scripts/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module.exports = Object.freeze({
/** @type {string} */
// Values: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
"PR_ACTION": process.env.PR_ACTION?.replace(/"/g, '') || "",

/** @type {string} */
"PR_AUTHOR": process.env.PR_AUTHOR?.replace(/"/g, '') || "",

/** @type {string} */
"PR_BODY": process.env.PR_BODY || "",

/** @type {string} */
"PR_TITLE": process.env.PR_TITLE || "",

/** @type {number} */
"PR_NUMBER": process.env.PR_NUMBER || 0,

/** @type {string} */
"PR_IS_MERGED": process.env.PR_IS_MERGED || "false",

/** @type {string} */
"LABEL_BLOCK": "do-not-merge",

/** @type {string} */
"LABEL_BLOCK_REASON": "need-issue",

/** @type {string} */
"LABEL_PENDING_RELEASE": "pending-release",

/** @type {string} */
"HANDLE_MAINTAINERS_TEAM": "@awslabs/aws-lambda-powertools-typescript",

/** @type {string[]} */
"IGNORE_AUTHORS": ["dependabot[bot]"],

/** @type {string[]} */
"AREAS": [
"tracer",
"metrics",
"logger",
],
});
26 changes: 26 additions & 0 deletions .github/scripts/download_pr_artifact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = async ({github, context, core}) => {
const fs = require('fs');

const workflowRunId = process.env.WORKFLOW_ID;
core.info(`Listing artifacts for workflow run ${workflowRunId}`);

const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: workflowRunId,
});

const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0];

core.info(`Downloading artifacts for workflow run ${workflowRunId}`);
const artifact = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});

core.info("Saving artifact found", artifact);

fs.writeFileSync('pr.zip', Buffer.from(artifact.data));
}
40 changes: 40 additions & 0 deletions .github/scripts/label_missing_related_issue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const {
PR_ACTION,
PR_AUTHOR,
PR_BODY,
PR_NUMBER,
IGNORE_AUTHORS,
LABEL_BLOCK,
LABEL_BLOCK_REASON
} = require("./constants");

module.exports = async ({github, context, core}) => {
if (IGNORE_AUTHORS.includes(PR_AUTHOR)) {
return core.notice("Author in IGNORE_AUTHORS list; skipping...");
}

if (PR_ACTION != "opened") {
return core.notice("Only newly open PRs are labelled to avoid spam; skipping");
}

const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?<issue>\d+)/;
const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY);
if (isMatch == null) {
core.info(`No related issue found, maybe the author didn't use the template but there is one.`);

let msg = "No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure.";
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
body: msg,
issue_number: PR_NUMBER,
});

return await github.rest.issues.addLabels({
issue_number: PR_NUMBER,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [LABEL_BLOCK, LABEL_BLOCK_REASON]
});
}
}
60 changes: 60 additions & 0 deletions .github/scripts/label_pr_based_on_title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const { PR_NUMBER, PR_TITLE, AREAS } = require("./constants");

module.exports = async ({github, context, core}) => {
const FEAT_REGEX = /feat(\((.+)\))?(:.+)/
const BUG_REGEX = /(fix|bug)(\((.+)\))?(:.+)/
const DOCS_REGEX = /(docs|doc)(\((.+)\))?(:.+)/
const CHORE_REGEX = /(chore)(\((.+)\))?(:.+)/
const DEPRECATED_REGEX = /(deprecated)(\((.+)\))?(:.+)/
const REFACTOR_REGEX = /(refactor)(\((.+)\))?(:.+)/

const labels = {
"feature": FEAT_REGEX,
"bug": BUG_REGEX,
"documentation": DOCS_REGEX,
"internal": CHORE_REGEX,
"enhancement": REFACTOR_REGEX,
"deprecated": DEPRECATED_REGEX,
};

// Maintenance: We should keep track of modified PRs in case their titles change
let miss = 0;
try {
for (const label in labels) {
const matcher = new RegExp(labels[label]);
const matches = matcher.exec(PR_TITLE);
if (matches != null) {
core.info(`Auto-labeling PR ${PR_NUMBER} with ${label}`);

await github.rest.issues.addLabels({
issue_number: PR_NUMBER,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [label]
});

const area = matches[2]; // second capture group contains the area
if (AREAS.indexOf(area) > -1) {
core.info(`Auto-labeling PR ${PR_NUMBER} with area ${area}`);
await github.rest.issues.addLabels({
issue_number: PR_NUMBER,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [`area/${area}`],
});
} else {
core.debug(`'${PR_TITLE}' didn't match any known area.`);
}

return;
} else {
core.debug(`'${PR_TITLE}' didn't match '${label}' semantic.`);
miss += 1;
}
}
} finally {
if (miss == Object.keys(labels).length) {
core.notice(`PR ${PR_NUMBER} title '${PR_TITLE}' doesn't follow semantic titles; skipping...`);
}
}
}
63 changes: 63 additions & 0 deletions .github/scripts/label_related_issue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const {
PR_AUTHOR,
PR_BODY,
PR_NUMBER,
IGNORE_AUTHORS,
LABEL_PENDING_RELEASE,
HANDLE_MAINTAINERS_TEAM,
PR_IS_MERGED,
} = require("./constants")

module.exports = async ({github, context, core}) => {
if (IGNORE_AUTHORS.includes(PR_AUTHOR)) {
return core.notice("Author in IGNORE_AUTHORS list; skipping...");
}

if (PR_IS_MERGED == "false") {
return core.notice("Only merged PRs to avoid spam; skipping");
}

const RELATED_ISSUE_REGEX = /Issue number:[^\d\r\n]+(?<issue>\d+)/;

const isMatch = RELATED_ISSUE_REGEX.exec(PR_BODY);

try {
if (!isMatch) {
core.setFailed(`Unable to find related issue for PR number ${PR_NUMBER}.\n\n Body details: ${PR_BODY}`);
return await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
body: `${HANDLE_MAINTAINERS_TEAM} No related issues found. Please ensure '${LABEL_PENDING_RELEASE}' label is applied before releasing.`,
issue_number: PR_NUMBER,
});
}
} catch (error) {
core.setFailed(`Unable to create comment on PR number ${PR_NUMBER}.\n\n Error details: ${error}`);
throw new Error(error);
}

const { groups: {issue} } = isMatch;

try {
core.info(`Auto-labeling related issue ${issue} for release`)
await github.rest.issues.addLabels({
issue_number: issue,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [LABEL_PENDING_RELEASE]
});
} catch (error) {
core.setFailed(`Is this issue number (${issue}) valid? Perhaps a discussion?`);
throw new Error(error);
}

const { groups: {relatedIssueNumber} } = isMatch;

core.info(`Auto-labeling related issue ${relatedIssueNumber} for release`);
return await github.rest.issues.addLabels({
issue_number: relatedIssueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [relatedIssueNumber]
});
}
13 changes: 13 additions & 0 deletions .github/scripts/save_pr_details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = async ({context, core}) => {
const fs = require('fs');
const filename = "pr.txt";

try {
fs.writeFileSync(`./${filename}`, JSON.stringify(context.payload));

return `PR successfully saved ${filename}`;
} catch (err) {
core.setFailed("Failed to save PR details");
console.error(err);
}
}
93 changes: 20 additions & 73 deletions .github/workflows/label_pr_on_title.yml
Original file line number Diff line number Diff line change
@@ -1,91 +1,38 @@
name: Label PR based on title

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

jobs:
label_pr:
runs-on: ubuntu-latest
get_pr_details:
# Guardrails to only ever run if PR recording workflow was indeed
# run in a PR event and ran successfully
if: >
${{ github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' }}
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/reusable_export_pr_details.yml
with:
record_pr_workflow_id: ${{ github.event.workflow_run.id }}
workflow_origin: ${{ github.event.repository.full_name }}
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
label_pr:
needs: get_pr_details
runs-on: ubuntu-latest
steps:
- name: 'Download artifact'
uses: actions/github-script@v6
# For security, we only download artifacts tied to the successful PR recording workflow
with:
script: |
const fs = require('fs');
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{github.event.workflow_run.id }},
});
const matchArtifact = artifacts.data.artifacts.filter(artifact => artifact.name == "pr")[0];
const artifact = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(artifact.data));
# NodeJS standard library doesn't provide ZIP capabilities; use system `unzip` command instead
- run: unzip pr.zip

- name: 'Label PR based on title'
- name: Checkout repository
uses: actions/checkout@v3
- name: "Label PR based on title"
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
PR_TITLE: ${{ needs.get_pr_details.outputs.prTitle }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# This safely runs in our base repo, not on fork
# thus allowing us to provide a write access token to label based on PR title
# and label PR based on semantic title accordingly
script: |
const fs = require('fs');
const pr_number = Number(fs.readFileSync('./number'));
const pr_title = fs.readFileSync('./title', 'utf-8').trim();
const FEAT_REGEX = /feat(\((\w+)\))?(\:.+)/
const BUG_REGEX = /(fix|bug)(\((\w+)\))?(\:.+)/
const DOCS_REGEX = /(docs|doc)(\((\w+)\))?(\:.+)/
const CHORE_REGEX = /(chore)(\((\w+)\))?(\:.+)/
const DEPRECATED_REGEX = /(deprecated)(\((\w+)\))?(\:.+)/
const REFACTOR_REGEX = /(refactor)(\((\w+)\))?(\:.+)/
const labels = {
"feature": FEAT_REGEX,
"bug": BUG_REGEX,
"documentation": DOCS_REGEX,
"internal": CHORE_REGEX,
"enhancement": REFACTOR_REGEX,
"deprecated": DEPRECATED_REGEX,
}
for (const label in labels) {
const matcher = new RegExp(labels[label])
const isMatch = matcher.exec(pr_title)
if (isMatch != null) {
console.info(`Auto-labeling PR ${pr_number} with ${label}`)
await github.rest.issues.addLabels({
issue_number: pr_number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [label]
})
break
}
}
const script = require('.github/scripts/label_pr_based_on_title.js')
await script({github, context, core})
Loading

0 comments on commit 6fe48ba

Please sign in to comment.