|
| 1 | +/** |
| 2 | + * @file Script that can be used to close stale theme PRs that have a `invalid` label. |
| 3 | + */ |
| 4 | +import * as dotenv from "dotenv"; |
| 5 | +dotenv.config(); |
| 6 | + |
| 7 | +import { debug, setFailed } from "@actions/core"; |
| 8 | +import github from "@actions/github"; |
| 9 | +import { RequestError } from "@octokit/request-error"; |
| 10 | +import { getGithubToken, getRepoInfo } from "./helpers.js"; |
| 11 | + |
| 12 | +// Script parameters |
| 13 | +const CLOSING_COMMENT = ` |
| 14 | + \rThis PR has been automatically closed due to inactivity. Please feel free to reopen it if you need to continue working on it.\ |
| 15 | + \rThank you for your contributions. |
| 16 | +`; |
| 17 | + |
| 18 | +/** |
| 19 | + * Fetch open PRs from a given repository. |
| 20 | + * @param user The user name of the repository owner. |
| 21 | + * @param repo The name of the repository. |
| 22 | + * @returns The open PRs. |
| 23 | + */ |
| 24 | +export const fetchOpenPRs = async (octokit, user, repo) => { |
| 25 | + const openPRs = []; |
| 26 | + let hasNextPage = true; |
| 27 | + let endCursor; |
| 28 | + while (hasNextPage) { |
| 29 | + try { |
| 30 | + const { repository } = await octokit.graphql( |
| 31 | + ` |
| 32 | + { |
| 33 | + repository(owner: "${user}", name: "${repo}") { |
| 34 | + open_prs: pullRequests(${ |
| 35 | + endCursor ? `after: "${endCursor}", ` : "" |
| 36 | + } |
| 37 | + first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) { |
| 38 | + nodes { |
| 39 | + number |
| 40 | + commits(last:1){ |
| 41 | + nodes{ |
| 42 | + commit{ |
| 43 | + pushedDate |
| 44 | + } |
| 45 | + } |
| 46 | + } |
| 47 | + labels(first: 100, orderBy:{field: CREATED_AT, direction: DESC}) { |
| 48 | + nodes { |
| 49 | + name |
| 50 | + } |
| 51 | + } |
| 52 | + reviews(first: 1, states: CHANGES_REQUESTED, author: "github-actions[bot]") { |
| 53 | + nodes { |
| 54 | + updatedAt |
| 55 | + } |
| 56 | + } |
| 57 | + } |
| 58 | + pageInfo { |
| 59 | + endCursor |
| 60 | + hasNextPage |
| 61 | + } |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + `, |
| 66 | + ); |
| 67 | + openPRs.push(...repository.open_prs.nodes); |
| 68 | + hasNextPage = repository.open_prs.pageInfo.hasNextPage; |
| 69 | + endCursor = repository.open_prs.pageInfo.endCursor; |
| 70 | + } catch (error) { |
| 71 | + if (error instanceof RequestError) { |
| 72 | + setFailed(`Could not retrieve top PRs using GraphQl: ${error.message}`); |
| 73 | + } |
| 74 | + throw error; |
| 75 | + } |
| 76 | + } |
| 77 | + return openPRs; |
| 78 | +}; |
| 79 | + |
| 80 | +/** |
| 81 | + * Retrieve pull requests that have a given label. |
| 82 | + * @param pull The pull requests to check. |
| 83 | + * @param label The label to check for. |
| 84 | + */ |
| 85 | +export const pullsWithLabel = (pulls, label) => { |
| 86 | + return pulls.filter((pr) => { |
| 87 | + return pr.labels.nodes.some((lab) => lab.name === label); |
| 88 | + }); |
| 89 | +}; |
| 90 | + |
| 91 | +/** |
| 92 | + * Check if PR is stale. Meaning that it hasn't been updated in a given time. |
| 93 | + * @param {Object} pullRequest request object. |
| 94 | + * @param {number} days number of days. |
| 95 | + * @returns Boolean indicating if PR is stale. |
| 96 | + */ |
| 97 | +const isStale = (pullRequest, staleDays) => { |
| 98 | + const lastCommitDate = new Date( |
| 99 | + pullRequest.commits.nodes[0].commit.pushedDate, |
| 100 | + ); |
| 101 | + if (pullRequest.reviews.nodes[0]) { |
| 102 | + const lastReviewDate = new Date(pullRequest.reviews.nodes[0].updatedAt); |
| 103 | + const lastUpdateDate = |
| 104 | + lastCommitDate >= lastReviewDate ? lastCommitDate : lastReviewDate; |
| 105 | + const now = new Date(); |
| 106 | + return now - lastUpdateDate > 1000 * 60 * 60 * 24 * staleDays; |
| 107 | + } else { |
| 108 | + return false; |
| 109 | + } |
| 110 | +}; |
| 111 | + |
| 112 | +/** |
| 113 | + * Main function. |
| 114 | + */ |
| 115 | +const run = async () => { |
| 116 | + try { |
| 117 | + // Create octokit client. |
| 118 | + const dryRun = process.env.DRY_RUN === "true" || false; |
| 119 | + const staleDays = process.env.STALE_DAYS || 15; |
| 120 | + debug("Creating octokit client..."); |
| 121 | + const octokit = github.getOctokit(getGithubToken()); |
| 122 | + const { owner, repo } = getRepoInfo(github.context); |
| 123 | + |
| 124 | + // Retrieve all theme pull requests. |
| 125 | + debug("Retrieving all theme pull requests..."); |
| 126 | + const prs = await fetchOpenPRs(octokit, owner, repo); |
| 127 | + const themePRs = pullsWithLabel(prs, "themes"); |
| 128 | + const invalidThemePRs = pullsWithLabel(themePRs, "invalid"); |
| 129 | + debug("Retrieving stale themePRs..."); |
| 130 | + const staleThemePRs = invalidThemePRs.filter((pr) => |
| 131 | + isStale(pr, staleDays), |
| 132 | + ); |
| 133 | + const staleThemePRsNumbers = staleThemePRs.map((pr) => pr.number); |
| 134 | + debug(`Found ${staleThemePRs.length} stale theme PRs`); |
| 135 | + |
| 136 | + // Loop through all stale invalid theme pull requests and close them. |
| 137 | + for (const prNumber of staleThemePRsNumbers) { |
| 138 | + debug(`Closing #${prNumber} because it is stale...`); |
| 139 | + if (!dryRun) { |
| 140 | + await octokit.issues.createComment({ |
| 141 | + owner, |
| 142 | + repo, |
| 143 | + issue_number: prNumber, |
| 144 | + body: CLOSING_COMMENT, |
| 145 | + }); |
| 146 | + await octokit.pulls.update({ |
| 147 | + owner, |
| 148 | + repo, |
| 149 | + pull_number: prNumber, |
| 150 | + state: "closed", |
| 151 | + }); |
| 152 | + } else { |
| 153 | + debug("Dry run enabled, skipping..."); |
| 154 | + } |
| 155 | + } |
| 156 | + } catch (error) { |
| 157 | + setFailed(error.message); |
| 158 | + } |
| 159 | +}; |
| 160 | + |
| 161 | +run(); |
0 commit comments