| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| const exec = require("@actions/exec"); | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const { Pr } = require("./pr"); | |
| const { ReviewersForLabel } = require("./reviewersForLabel"); | |
| const { BOT_NAME, PR_STATE_DIR } = require("./constants"); | |
| function getPrFileName(prNumber) { | |
| return `pr-${prNumber}.json`.toLowerCase(); | |
| } | |
| function getReviewersForLabelFileName(label) { | |
| return `reviewers-for-label-${label}.json`.toLowerCase(); | |
| } | |
| async function commitStateToRepo() { | |
| try { | |
| await exec.exec("git pull origin pr-bot-state"); | |
| } catch (err) { | |
| console.log( | |
| `Unable to get most recent repo contents, commit may fail: ${err}` | |
| ); | |
| } | |
| // Print changes for observability | |
| await exec.exec("git status", [], { ignoreReturnCode: true }); | |
| await exec.exec("git add -A state"); | |
| const changes = await exec.exec( | |
| "git diff --quiet --cached origin/pr-bot-state state", | |
| [], | |
| { ignoreReturnCode: true } | |
| ); | |
| if (changes == 1) { | |
| await exec.exec(`git commit -m "Updating config from bot" --allow-empty`); | |
| await exec.exec("git push origin pr-bot-state"); | |
| } else { | |
| console.log( | |
| "Skipping updating state branch since there are no changes to commit" | |
| ); | |
| } | |
| } | |
| export class PersistentState { | |
| private switchedBranch = false; | |
| // Returns a Pr object representing the current saved state of the pr. | |
| async getPrState(prNumber: number): Promise<typeof Pr> { | |
| var fileName = getPrFileName(prNumber); | |
| return new Pr(await this.getState(fileName, PR_STATE_DIR)); | |
| } | |
| // Writes a Pr object representing the current saved state of the pr to persistent storage. | |
| async writePrState(prNumber: number, newState: any) { | |
| var fileName = getPrFileName(prNumber); | |
| await this.writeState(fileName, PR_STATE_DIR, new Pr(newState)); | |
| } | |
| // Returns a ReviewersForLabel object representing the current saved state of which reviewers have reviewed recently. | |
| async getReviewersForLabelState( | |
| label: string | |
| ): Promise<typeof ReviewersForLabel> { | |
| var fileName = getReviewersForLabelFileName(label); | |
| return new ReviewersForLabel(label, await this.getState(fileName, "state")); | |
| } | |
| // Writes a ReviewersForLabel object representing the current saved state of which reviewers have reviewed recently. | |
| async writeReviewersForLabelState(label: string, newState: any) { | |
| var fileName = getReviewersForLabelFileName(label); | |
| await this.writeState( | |
| fileName, | |
| "state", | |
| new ReviewersForLabel(label, newState) | |
| ); | |
| } | |
| // Deletes up to maxToDelete state files for PRs that are no longer open, starting from the oldest PRs. | |
| async deleteStalePrStates( | |
| openPulls: any[], | |
| maxToDelete: number = 100 | |
| ): Promise<number> { | |
| if (openPulls.length === 0) { | |
| return 0; | |
| } | |
| await this.ensureCorrectBranch(); | |
| if (!fs.existsSync(PR_STATE_DIR)) { | |
| return 0; | |
| } | |
| const openPrSet = new Set(openPulls.map((p) => p.number)); | |
| const files = fs.readdirSync(PR_STATE_DIR); | |
| const stalePrs: { prNumber: number; filePath: string }[] = []; | |
| for (const file of files) { | |
| const match = file.match(/^pr-(\d+)\.json$/); | |
| if (match) { | |
| const prNumber = parseInt(match[1], 10); | |
| if (!openPrSet.has(prNumber)) { | |
| stalePrs.push({ | |
| prNumber, | |
| filePath: path.join(PR_STATE_DIR, file), | |
| }); | |
| } | |
| } | |
| } | |
| // Sort by PR number ascending so the oldest PRs are deleted first | |
| stalePrs.sort((a, b) => a.prNumber - b.prNumber); | |
| const prsToDelete = stalePrs.slice(0, maxToDelete); | |
| for (const pr of prsToDelete) { | |
| fs.unlinkSync(pr.filePath); | |
| } | |
| if (prsToDelete.length > 0) { | |
| console.log( | |
| `Deleted ${prsToDelete.length} stale PR state files (oldest: PR ${ | |
| prsToDelete[0].prNumber | |
| }, newest: PR ${prsToDelete[prsToDelete.length - 1].prNumber})` | |
| ); | |
| await commitStateToRepo(); | |
| } | |
| return prsToDelete.length; | |
| } | |
| private async getState(fileName, baseDirectory) { | |
| await this.ensureCorrectBranch(); | |
| fileName = path.join(baseDirectory, fileName); | |
| if (!fs.existsSync(fileName)) { | |
| return null; | |
| } | |
| return JSON.parse(fs.readFileSync(fileName, { encoding: "utf-8" })); | |
| } | |
| private async writeState(fileName, baseDirectory, state) { | |
| await this.ensureCorrectBranch(); | |
| fileName = path.join(baseDirectory, fileName); | |
| if (!fs.existsSync(baseDirectory)) { | |
| fs.mkdirSync(baseDirectory, { recursive: true }); | |
| } | |
| fs.writeFileSync(fileName, JSON.stringify(state, null, 2), { | |
| encoding: "utf-8", | |
| }); | |
| await commitStateToRepo(); | |
| } | |
| private async ensureCorrectBranch() { | |
| if (this.switchedBranch) { | |
| return; | |
| } | |
| console.log( | |
| "Switching to branch pr-bot-state for reading/storing persistent state between runs" | |
| ); | |
| try { | |
| await exec.exec(`git config user.name ${BOT_NAME}`); | |
| await exec.exec(`git config user.email ${BOT_NAME}@github.com`); | |
| await exec.exec("git config pull.rebase false"); | |
| await exec.exec("git fetch origin pr-bot-state --depth=1"); | |
| await exec.exec("git checkout pr-bot-state"); | |
| } catch { | |
| console.log( | |
| "Couldnt find branch pr-bot-state in origin, trying to create it" | |
| ); | |
| await exec.exec("git checkout -b pr-bot-state"); | |
| // Ensure that if we've created or just checked out the branch that we can also push to it | |
| await exec.exec("git push origin pr-bot-state"); | |
| } | |
| this.switchedBranch = true; | |
| } | |
| } |