ci: add stale theme pull request closer action push (#2067)

This commit adds the `stale-theme-pr-closer` closer action. This action
is used to close theme pull requests with an `invalid` label that has
been stale for a given number of `STALE_DAYS`.
This commit is contained in:
Rick Staa 2022-10-02 11:05:13 +02:00 committed by GitHub
parent 986070a597
commit b8faef6857
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 242 additions and 43 deletions

View File

@ -3,10 +3,11 @@ on:
deployment_status:
jobs:
preview:
e2eTests:
if:
github.event_name == 'deployment_status' &&
github.event.deployment_status.state == 'success'
name: Perform 2e2 tests
runs-on: ubuntu-latest
strategy:
matrix:

View File

@ -12,6 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3 # NOTE: Retrieve issue templates.
- name: Run empty issues closer action
uses: rickstaa/empty-issues-closer-action@v1
env:

View File

@ -1,5 +1,4 @@
name: Generate Theme Readme
on:
push:
branches:
@ -8,8 +7,9 @@ on:
- "themes/index.js"
jobs:
build:
generateThemeDoc:
runs-on: ubuntu-latest
name: Generate theme doc
strategy:
matrix:
node-version: [16.x]

View File

@ -1,5 +1,4 @@
name: Theme preview
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
@ -10,11 +9,11 @@ on:
jobs:
previewTheme:
name: Install & Preview
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
name: Install & Preview
steps:
- uses: actions/checkout@v3

View File

@ -0,0 +1,30 @@
name: Close stale theme pull requests that have the 'invalid' label.
on:
schedule:
- cron: "0 0 */7 * *"
jobs:
closeOldThemePrs:
name: Close stale 'invalid' theme PRs
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: npm
- uses: bahmutov/npm-install@v1
with:
useLockFile: false
- run: npm run close-stale-theme-prs
env:
STALE_DAYS: 15
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,5 +1,4 @@
name: Test
on:
push:
branches:
@ -10,6 +9,7 @@ on:
jobs:
build:
name: Perform tests
runs-on: ubuntu-latest
strategy:
matrix:

View File

@ -11,6 +11,7 @@
"test:e2e": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.e2e.config.js",
"theme-readme-gen": "node scripts/generate-theme-doc",
"preview-theme": "node scripts/preview-theme",
"close-stale-theme-prs": "node scripts/close-stale-theme-prs",
"generate-langs-json": "node scripts/generate-langs-json",
"format": "./node_modules/.bin/prettier --write .",
"format:check": "./node_modules/.bin/prettier --check ."

View File

@ -0,0 +1,161 @@
/**
* @file Script that can be used to close stale theme PRs that have a `invalid` label.
*/
import * as dotenv from "dotenv";
dotenv.config();
import { debug, setFailed } from "@actions/core";
import github from "@actions/github";
import { RequestError } from "@octokit/request-error";
import { getGithubToken, getRepoInfo } from "./helpers.js";
// Script parameters
const CLOSING_COMMENT = `
\rThis PR has been automatically closed due to inactivity. Please feel free to reopen it if you need to continue working on it.\
\rThank you for your contributions.
`;
/**
* Fetch open PRs from a given repository.
* @param user The user name of the repository owner.
* @param repo The name of the repository.
* @returns The open PRs.
*/
export const fetchOpenPRs = async (octokit, user, repo) => {
const openPRs = [];
let hasNextPage = true;
let endCursor;
while (hasNextPage) {
try {
const { repository } = await octokit.graphql(
`
{
repository(owner: "${user}", name: "${repo}") {
open_prs: pullRequests(${
endCursor ? `after: "${endCursor}", ` : ""
}
first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
number
commits(last:1){
nodes{
commit{
pushedDate
}
}
}
labels(first: 100, orderBy:{field: CREATED_AT, direction: DESC}) {
nodes {
name
}
}
reviews(first: 1, states: CHANGES_REQUESTED, author: "github-actions[bot]") {
nodes {
updatedAt
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
`,
);
openPRs.push(...repository.open_prs.nodes);
hasNextPage = repository.open_prs.pageInfo.hasNextPage;
endCursor = repository.open_prs.pageInfo.endCursor;
} catch (error) {
if (error instanceof RequestError) {
setFailed(`Could not retrieve top PRs using GraphQl: ${error.message}`);
}
throw error;
}
}
return openPRs;
};
/**
* Retrieve pull requests that have a given label.
* @param pull The pull requests to check.
* @param label The label to check for.
*/
export const pullsWithLabel = (pulls, label) => {
return pulls.filter((pr) => {
return pr.labels.nodes.some((lab) => lab.name === label);
});
};
/**
* Check if PR is stale. Meaning that it hasn't been updated in a given time.
* @param {Object} pullRequest request object.
* @param {number} days number of days.
* @returns Boolean indicating if PR is stale.
*/
const isStale = (pullRequest, staleDays) => {
const lastCommitDate = new Date(
pullRequest.commits.nodes[0].commit.pushedDate,
);
if (pullRequest.reviews.nodes[0]) {
const lastReviewDate = new Date(pullRequest.reviews.nodes[0].updatedAt);
const lastUpdateDate =
lastCommitDate >= lastReviewDate ? lastCommitDate : lastReviewDate;
const now = new Date();
return now - lastUpdateDate > 1000 * 60 * 60 * 24 * staleDays;
} else {
return false;
}
};
/**
* Main function.
*/
const run = async () => {
try {
// Create octokit client.
const dryRun = process.env.DRY_RUN === "true" || false;
const staleDays = process.env.STALE_DAYS || 15;
debug("Creating octokit client...");
const octokit = github.getOctokit(getGithubToken());
const { owner, repo } = getRepoInfo(github.context);
// Retrieve all theme pull requests.
debug("Retrieving all theme pull requests...");
const prs = await fetchOpenPRs(octokit, owner, repo);
const themePRs = pullsWithLabel(prs, "themes");
const invalidThemePRs = pullsWithLabel(themePRs, "invalid");
debug("Retrieving stale themePRs...");
const staleThemePRs = invalidThemePRs.filter((pr) =>
isStale(pr, staleDays),
);
const staleThemePRsNumbers = staleThemePRs.map((pr) => pr.number);
debug(`Found ${staleThemePRs.length} stale theme PRs`);
// Loop through all stale invalid theme pull requests and close them.
for (const prNumber of staleThemePRsNumbers) {
debug(`Closing #${prNumber} because it is stale...`);
if (!dryRun) {
await octokit.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: CLOSING_COMMENT,
});
await octokit.pulls.update({
owner,
repo,
pull_number: prNumber,
state: "closed",
});
} else {
debug("Dry run enabled, skipping...");
}
}
} catch (error) {
setFailed(error.message);
}
};
run();

40
scripts/helpers.js Normal file
View File

@ -0,0 +1,40 @@
/**
* @file Contains helper functions used in the scripts.
*/
// Script variables.
const OWNER = "anuraghazra";
const REPO = "github-readme-stats";
/**
* Retrieve information about the repository that ran the action.
*
* @param {Object} context Action context.
* @returns {Object} Repository information.
*/
export const getRepoInfo = (ctx) => {
try {
return {
owner: ctx.repo.owner,
repo: ctx.repo.repo,
};
} catch (error) {
return {
owner: OWNER,
repo: REPO,
};
}
};
/**
* Retrieve github token and throw error if it is not found.
*
* @returns {string} Github token.
*/
export const getGithubToken = () => {
const token = core.getInput("github_token") || process.env.GITHUB_TOKEN;
if (!token) {
throw Error("Could not find github token");
}
return token;
};

View File

@ -4,7 +4,7 @@
import * as dotenv from "dotenv";
dotenv.config();
import core, { debug, setFailed } from "@actions/core";
import { debug, setFailed } from "@actions/core";
import github from "@actions/github";
import ColorContrastChecker from "color-contrast-checker";
import { info } from "console";
@ -14,10 +14,9 @@ import parse from "parse-diff";
import { inspect } from "util";
import { isValidHexColor } from "../src/common/utils.js";
import { themes } from "../themes/index.js";
import { getGithubToken, getRepoInfo } from "./helpers.js";
// Script variables
const OWNER = "anuraghazra";
const REPO = "github-readme-stats";
// Script variables.
const COMMENTER = "github-actions[bot]";
const COMMENT_TITLE = "Automated Theme Preview";
@ -43,26 +42,6 @@ const REQUIRED_COLOR_PROPS = [
const INVALID_REVIEW_COMMENT = (commentUrl) =>
`Some themes are invalid. See the [Automated Theme Preview](${commentUrl}) comment above for more information.`;
/**
* Retrieve information about the repository that ran the action.
*
* @param {Object} context Action context.
* @returns {Object} Repository information.
*/
export const getRepoInfo = (ctx) => {
try {
return {
owner: ctx.repo.owner,
repo: ctx.repo.repo,
};
} catch (error) {
return {
owner: OWNER,
repo: REPO,
};
}
};
/**
* Retrieve PR number from the event payload.
*
@ -86,19 +65,6 @@ const getCommenter = () => {
return process.env.COMMENTER ? process.env.COMMENTER : COMMENTER;
};
/**
* Retrieve github token and throw error if it is not found.
*
* @returns {string} Github token.
*/
const getGithubToken = () => {
const token = core.getInput("github_token") || process.env.GITHUB_TOKEN;
if (!token) {
throw Error("Could not find github token");
}
return token;
};
/**
* Returns whether the comment is a preview comment.
*