Skip to content

Commit

Permalink
ci: add type checking to github scripts (#9851)
Browse files Browse the repository at this point in the history
## Summary

- Add
[`actions/github-script`](https://github.com/marketplace/actions/github-script)
types
- Add
[`@octokit/webhook-types`](https://www.npmjs.com/package/@octokit/webhooks-types)
for improved payload typing
- Use JSDoc wizardry to typecheck the JavaScript files used by our
workflows

## Notes

I got a husky error when installing the `github-script` types. Here is a
related issue: typicode/husky#851

Upgrading husky resolves the error, so I created a pull request:
actions/github-script#482

I installed `github-script` using the PR number for now. We can unpin
the PR once it's merged by re-installing via the [command in the
doc](https://github.com/actions/github-script/#use-scripts-with-jsdoc-support):

```sh
npm i -D @types/github-script@github:actions/github-script
```
  • Loading branch information
benelan authored Jul 25, 2024
1 parent 3edd658 commit 14f8cff
Show file tree
Hide file tree
Showing 13 changed files with 607 additions and 56 deletions.
12 changes: 7 additions & 5 deletions .github/scripts/addCalcitePackageLabel.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// @ts-check
const { createLabelIfMissing } = require("./support/utils");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').IssuesEvent} */ (context.payload);
const {
repo: { owner, repo },
payload: {
issue: { body, number: issue_number },
},
} = context;
issue: { body, number: issue_number },
} = payload;

if (!body) {
console.log("could not determine the issue body");
Expand Down
16 changes: 9 additions & 7 deletions .github/scripts/addEsriProductLabel.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// @ts-check
const { createLabelIfMissing } = require("./support/utils");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').IssuesEvent} */ (context.payload);
const {
repo: { owner, repo },
payload: {
action,
issue: { body, number: issue_number },
},
} = context;
action,
issue: { body, number: issue_number },
} = payload;

if (!body) {
console.log("could not determine the issue body");
Expand All @@ -28,7 +30,7 @@ module.exports = async ({ github, context }) => {

const product = (productRegexMatch && productRegexMatch[0] ? productRegexMatch[0] : "").trim();

if (product && product !== "N/A") {
if (product !== "N/A") {
await createLabelIfMissing({
github,
context,
Expand Down
14 changes: 8 additions & 6 deletions .github/scripts/addPriorityLabel.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// @ts-check
const { createLabelIfMissing } = require("./support/utils");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').IssuesEvent} */ (context.payload);
const {
repo: { owner, repo },
payload: {
action,
issue: { body, number: issue_number },
},
} = context;
action,
issue: { body, number: issue_number },
} = payload;

if (!body) {
console.log("could not determine the issue body");
Expand Down
27 changes: 20 additions & 7 deletions .github/scripts/assignForVerification.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
// @ts-check
const { handoff, issueWorkflow } = require("./support/resources");
const { removeLabel } = require("./support/utils");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').IssuesLabeledEvent} */ (context.payload);
const {
label,
issue: { number },
} = payload;

const { ISSUE_VERIFIERS, CALCITE_DESIGNERS } = process.env;
const { label } = context.payload;

if (label && label.name === issueWorkflow.installed) {
if (label?.name === issueWorkflow.installed) {
const issueProps = {
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
owner,
repo,
issue_number: number,
};

const { data: issue } = await github.rest.issues.get(issueProps);

await removeLabel({ github, context, label: issueWorkflow.inDevelopment });

const assignees = ISSUE_VERIFIERS.split(",").map((v) => v.trim());
const assignees = ISSUE_VERIFIERS?.split(",").map((v) => v.trim());

// assign designers if figma updates are required
if (issue.labels.map((label) => label.name).includes(handoff.figmaChanges)) {
if (
assignees &&
CALCITE_DESIGNERS &&
issue.labels.map((label) => (typeof label === "string" ? label : label.name)).includes(handoff.figmaChanges)
) {
assignees.push(...CALCITE_DESIGNERS.split(",").map((v) => v.trim()));
}

Expand Down
19 changes: 13 additions & 6 deletions .github/scripts/assignPullRequestAuthor.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
// @ts-check
/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').PullRequestEvent} */ (context.payload);
const {
assignees,
number,
user: { login: author },
} = context.payload.pull_request;
pull_request: {
assignees,
number,
user: { login: author },
},
} = payload;

const updatedAssignees =
assignees && assignees.length ? [...assignees.map((a) => a.login).filter((a) => a !== author), author] : [author];

try {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
owner,
repo,
issue_number: number,
assignees: updatedAssignees,
});
Expand Down
16 changes: 13 additions & 3 deletions .github/scripts/labelPullRequestWithCommitType.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// @ts-check
const { issueType } = require("./support/resources");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { title, number } = context.payload.pull_request;
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').PullRequestEvent} */ (context.payload);
const {
pull_request: { title, number },
} = payload;

const conventionalCommitRegex =
/^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([\w ,-]+\))?(!?:\s+)([\w ]+[\s\S]*)/i;
Expand All @@ -23,16 +30,19 @@ module.exports = async ({ github, context }) => {

try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [typeLabel],
});
} catch (e) {
console.error("Unable to label pull request, the author likely does not have write permissions\n", e);
}
};

/**
* @param {string} type
*/
function getLabelName(type) {
switch (type) {
case "feat":
Expand Down
26 changes: 21 additions & 5 deletions .github/scripts/notifyAboutNewComponent.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
// @ts-check
// If the "new component" label is added to an issue, generates a notification to the Calcite designer lead(s)
// The secret is formatted like so: designer1, designer2, designer3
// Note the script automatically adds the "@" character in to notify the designer lead(s)

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { designers } = process.env;
const { repo, owner } = context.repo;

const payload = /** @type {import('@octokit/webhooks-types').IssuesLabeledEvent} */ (context.payload);
const {
issue: { number },
} = payload;

const { DESIGNERS } = process.env;

// Add a "@" character to notify the user
const calcite_designers = designers.split(",").map((v) => " @" + v.trim());
const calcite_designers = DESIGNERS?.split(",").map((v) => " @" + v.trim());

if (!calcite_designers?.length) {
console.error("unable to determine designers");
process.exit(1);
}

// Add a comment to issues with the 'new component' label to notify the designer(s)
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
owner,
repo,
issue_number: number,
body: `cc ${calcite_designers}`,
});
};
23 changes: 16 additions & 7 deletions .github/scripts/notifyWhenReadyForDev.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
// When the "ready for dev" label is added to an issue:
// 1. Modifies the labels,
// 2. Updates the assignees and milestone, and
Expand All @@ -9,18 +10,26 @@
const { issueWorkflow, planning } = require("./support/resources");
const { removeLabel } = require("./support/utils");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { managers } = process.env;
const { label } = context.payload;
const { repo, owner } = context.repo;

if (label && label.name === "ready for dev") {
const payload = /** @type {import('@octokit/webhooks-types').IssuesLabeledEvent} */ (context.payload);
const {
issue: { number },
label,
} = payload;

const { MANAGERS } = process.env;

if (label?.name === "ready for dev") {
// Add a "@" character to notify the user
const calcite_managers = managers.split(",").map((v) => " @" + v.trim());
const calcite_managers = MANAGERS?.split(",").map((v) => " @" + v.trim());

const issueProps = {
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
owner,
repo,
issue_number: number,
};

/* Modify labels */
Expand Down
23 changes: 16 additions & 7 deletions .github/scripts/notifyWhenSpikeComplete.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
// When the "spike complete" label is added to an issue:
// 1. Modifies the labels,
// 2. Updates the assignees and milestone, and
Expand All @@ -9,18 +10,26 @@
const { issueWorkflow, planning } = require("./support/resources");
const { removeLabel } = require("./support/utils");

/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ github, context }) => {
const { managers } = process.env;
const { label } = context.payload;
const { repo, owner } = context.repo;

if (label && label.name === planning.spikeComplete) {
const payload = /** @type {import('@octokit/webhooks-types').IssuesLabeledEvent} */ (context.payload);
const {
issue: { number },
label,
} = payload;

const { MANAGERS } = process.env;

if (label?.name === planning.spikeComplete) {
// Add a "@" character to notify the user
const calcite_managers = managers.split(",").map((v) => " @" + v.trim());
const calcite_managers = MANAGERS?.split(",").map((v) => " @" + v.trim());

const issueProps = {
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
owner,
repo,
issue_number: number,
};

/* Modify labels */
Expand Down
19 changes: 19 additions & 0 deletions .github/scripts/support/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
// @ts-check
module.exports = {
/**
* @typedef {object} removeLabelParam
* @property {InstanceType<typeof import('@actions/github/lib/utils').GitHub>} github
* @property {import('@actions/github/lib/context').Context} context
* @property {string} label
*
* @param {removeLabelParam} obj
**/
removeLabel: async ({ github, context, label }) => {
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
Expand All @@ -15,6 +24,16 @@ module.exports = {
}
},

/**
* @typedef {object} createLabelIfMissingParam
* @property {InstanceType<typeof import('@actions/github/lib/utils').GitHub>} github
* @property {import('@actions/github/lib/context').Context} context
* @property {string} label
* @property {string} color
* @property {string} description
*
* @param {createLabelIfMissingParam} obj
**/
createLabelIfMissing: async ({ github, context, label, color, description }) => {
const { owner, repo } = context.repo;
try {
Expand Down
14 changes: 11 additions & 3 deletions .github/scripts/validatePullRequestTitle.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
// @ts-check
/** @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments */
module.exports = async ({ context, core }) => {
const payload = /** @type {import('@octokit/webhooks-types').PullRequestEvent} */ (context.payload);
const { title } = payload.pull_request;

const REGEX = new RegExp("^[^…]+$"); // Title must match this regex
const MIN_LENGTH = 1; // Min length of the title
const MAX_LENGTH = -1; // Max length of the title (-1 is no max)
const ALLOWED_PREFIXES = []; // Title must start with one of these prefixes
const DISALLOWED_PREFIXES = []; // Title cannot start with one of these prefixes
const PREFIX_CASE_SENSITIVE = false; // Whether the prefix is case sensitive

/**
* @param {string} title
* @param {string} prefix
*/
const validateTitlePrefix = (title, prefix) =>
PREFIX_CASE_SENSITIVE ? title.startsWith(prefix) : title.toLowerCase().startsWith(prefix.toLowerCase());

const { title } = context.payload.pull_request;
if (!REGEX.test(title)) {
core.setFailed(`Pull Request title "${title}" failed to match regex - ${REGEX}`);
return;
Expand All @@ -28,15 +36,15 @@ module.exports = async ({ context, core }) => {
core.info(`Allowed Prefixes: ${ALLOWED_PREFIXES}`);
if (ALLOWED_PREFIXES.length && !ALLOWED_PREFIXES.some((prefix) => validateTitlePrefix(title, prefix))) {
core.setFailed(
`Pull Request title "${title}" did not start with any of the required prefixes - ${ALLOWED_PREFIXES}`
`Pull Request title "${title}" did not start with any of the required prefixes - ${ALLOWED_PREFIXES}`,
);
return;
}

core.info(`Disallowed Prefixes: ${DISALLOWED_PREFIXES}`);
if (DISALLOWED_PREFIXES.length && DISALLOWED_PREFIXES.some((prefix) => validateTitlePrefix(title, prefix))) {
core.setFailed(
`Pull Request title "${title}" started with one of the disallowed prefixes - ${DISALLOWED_PREFIXES}`
`Pull Request title "${title}" started with one of the disallowed prefixes - ${DISALLOWED_PREFIXES}`,
);
return;
}
Expand Down
Loading

0 comments on commit 14f8cff

Please sign in to comment.