From 73aedd17c9deda523bcafdfe96104f0a84ce5838 Mon Sep 17 00:00:00 2001 From: Dominique Hazael-Massieux Date: Wed, 19 Jun 2024 12:39:53 +0200 Subject: [PATCH] Add automation to close outdated pull requests Some error reports are pending regeneration of the spec (and labeled as such). This GitHub workflow automatically closes their pull requests (and deletes the underlying branch) once it detects the relevant spec has been updated. At that point, either the errors will have been fixed by the regeneration, or a new report will be generated at the next analysis. --- .github/workflows/clean-pending-reports.yml | 29 +++++ src/reporting/clean-pending-regeneration.js | 128 ++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 .github/workflows/clean-pending-reports.yml create mode 100644 src/reporting/clean-pending-regeneration.js diff --git a/.github/workflows/clean-pending-reports.yml b/.github/workflows/clean-pending-reports.yml new file mode 100644 index 00000000..7887f0b3 --- /dev/null +++ b/.github/workflows/clean-pending-reports.yml @@ -0,0 +1,29 @@ +name: Close potentially outdated report pull requests + +on: + schedule: + - cron: '30 4 * * *' + workflow_dispatch: +jobs: + clean: + runs-on: ubuntu-latest + steps: + - name: Setup node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Checkout strudy + uses: actions/checkout@v4 + - name: Install dependencies + run: | + npm ci + - name: Checkout webref + uses: actions/checkout@v4 + with: + repository: w3c/webref + path: webref + - name: Close pull requests if needed + run: node src/reporting/clean-pending-regeneration.js webref/ed/ + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/src/reporting/clean-pending-regeneration.js b/src/reporting/clean-pending-regeneration.js new file mode 100644 index 00000000..0f01c253 --- /dev/null +++ b/src/reporting/clean-pending-regeneration.js @@ -0,0 +1,128 @@ +/** + * Check GitHub pull requests labeled "pending-spec-regeneration" + * and close them if the relevant spec has been regenerated + * since the PR was created. + */ + +const core = require('@actions/core'); +const Octokit = require('../lib/octokit'); +const fs = require('fs'); +const path = require('path'); +const matter = require('gray-matter'); +const { loadCrawlResults } = require('../lib/util'); + +const owner = 'w3c'; +const repo = 'strudy'; + + + +/** + * Check GitHub issues and PR referenced by patch files and drop patch files + * that only reference closed issues and PR. + * + * @function + * @return {String} A GitHub flavored markdown string that describes what + * patches got dropped and why. To be used in a possible PR. Returns an + * empty string when there are no patches to drop. + */ +async function dropPendingReportsWhenPossible (edCrawlResultsPath) { + // Find PRs labeled pending-spec-regenetation + const { data: issues } = (await octokit.rest.issues.listForRepo({ + owner, + repo, + labels: "pending-spec-regeneration" + })); + + const pull_requests = issues.filter(i => i.pull_request); + if (!pull_requests.length) { + console.log("No open pull requests labeled pending-spec-regeneration"); + return []; + } + const crawl = await loadCrawlResults(edCrawlResultsPath); + + const dropped = []; + for (const pr of pull_requests) { + // find relevant spec URL from issue body (the first markdown link there) + const m = pr.body.match(/\]\(([^\)]+)\)/); + if (!m) { + console.error(`Could not find link to spec in body of PR ${pr.number}, aborting`); + continue; + } + const url = m[1]; + const spec = crawl.ed.find(s => s.nightly?.url === url); + if (!spec) { + console.error(`Could not find ${url} in webref crawl data, aborting`); + continue; + } + const lastModified = new Date(spec.crawlCacheInfo.lastModified); + // check if the last modified date in webref is later than the PR creation + if (lastModified.toJSON() > pr.created_at) { + console.log(`PR ${pr.number} may no longer be relevant, closing it`); + // if it is, delete the branch, and close the PR + const prData = await octokit.rest.pulls.get({ + owner, + repo, + pull_number: pr.number + }); + await octokit.rest.git.deleteRef({ + owner, + repo, + ref: prData.head.ref + }); + await octokit.rest.issues.update({ + owner, + repo, + issue_number: pr.number, + state: "closed" + }); + dropped.push(pr.pull_request.html_url); + } + } + return dropped; +} + +/******************************************************************************* +Retrieve GH_TOKEN from environment, prepare Octokit and kick things off +*******************************************************************************/ +const GH_TOKEN = (() => { + try { + return require('../../config.json').GH_TOKEN; + } catch { + return process.env.GH_TOKEN; + } +})(); +if (!GH_TOKEN) { + console.error('GH_TOKEN must be set to some personal access token as an env variable or in a config.json file'); + process.exit(1); +} + +const octokit = new Octokit({ + auth: GH_TOKEN + // log: console +}); + +let edCrawlResultsPath = process.argv[2]; + +if (!edCrawlResultsPath) { + console.error('Path to the webref crawl of editors draft needs to be provided as argument'); + process.exit(1); +} + +// Target the index file if needed +if (!edCrawlResultsPath.endsWith('index.json')) { + edCrawlResultsPath = path.join(edCrawlResultsPath, 'index.json'); +} + + +dropPendingReportsWhenPossible(edCrawlResultsPath) + .then(res => { + core.exportVariable('dropped_reports', res); + console.log(); + console.log('Set dropped_reports env variable'); + console.log(res); + console.log('== The end =='); + }) + .catch(err => { + console.error(err); + process.exit(1); + });