Skip to content

chore(ci): housekeeping scripts & workflows related to PRs #1023

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 3, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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