diff --git a/.github/workflows/clean-patches.yml b/.github/workflows/clean-patches.yml new file mode 100644 index 000000000000..53023353bfca --- /dev/null +++ b/.github/workflows/clean-patches.yml @@ -0,0 +1,37 @@ +name: Clean patches when issues/PR are closed + +on: + schedule: + - cron: '30 3 * * 3' + workflow_dispatch: +jobs: + clean: + runs-on: ubuntu-18.04 + steps: + - name: Checkout webref + uses: actions/checkout@master + + - name: Setup node.js + uses: actions/setup-node@master + with: + node-version: 14.x + + - name: Install dependencies + run: | + npm install @actions/core + npm install @octokit/rest + + - name: Drop patches locally when related issues/PR are closed + run: node tools/clean-patches.js + + - name: Create PR to drop patches from repo if needed + uses: peter-evans/create-pull-request@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: Drop patches that are no longer needed + commit-message: "Drop patches that are no longer needed" + body: ${{ env.dropped_patches }} + assignees: tidoust, dontcallmedom + branch: clean-patches + branch-suffix: timestamp diff --git a/package-lock.json b/package-lock.json index 4d14eb197b4c..a296a39c189e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@actions/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.3.0.tgz", + "integrity": "sha512-xxtX0Cwdhb8LcgatfJkokqT8KzPvcIbwL9xpLU09nOwBzaStbfm0dNncsP0M4us+EpoPdWy7vbzU5vSOH7K6pg==", + "dev": true + }, "@jsdevtools/ez-spawn": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", diff --git a/package.json b/package.json index dccf07e7c972..4cd12e0be9aa 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "node": ">=10" }, "devDependencies": { + "@actions/core": "1.3.0", "@jsdevtools/npm-publish": "1.4.3", "@octokit/rest": "18.5.3", "@webref/css": "file:packages/css", diff --git a/tools/clean-patches.js b/tools/clean-patches.js new file mode 100644 index 000000000000..346a48f9def5 --- /dev/null +++ b/tools/clean-patches.js @@ -0,0 +1,134 @@ +/** + * Check GitHub issues and pull requests referenced by patches and create + * a pull request to drop patches that should no longer be needed. + */ + +const core = require('@actions/core'); +const { Octokit } = require("@octokit/rest"); +const fs = require("fs"); +const path = require("path"); + + +/** + * 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 dropPatchesWhenPossible() { + const rootDir = path.join(__dirname, "..", "ed"); + + console.log("Gather patch files"); + let patches = []; + const subDirs = fs.readdirSync(rootDir); + for (const subDir of subDirs) { + if (subDir.endsWith("patches")) { + const files = fs.readdirSync(path.join(rootDir, subDir)); + for (const file of files) { + if (file.endsWith(".patch")) { + const patch = path.join(subDir, file); + console.log(`- add "${patch}"`); + patches.push({ name: patch }); + } + } + } + } + + console.log(); + console.log("Extract list of issues"); + const diffStart = /^---$/m; + const issueUrl = /(?:^|\s)https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)(?:\s|$)/g; + for (const patch of patches) { + const contents = fs.readFileSync(path.join(rootDir, patch.name), "utf8"); + const desc = contents.substring(0, contents.match(diffStart)?.index); + const patchIssues = [...desc.matchAll(issueUrl)]; + for (patchIssue of patchIssues) { + if (!patch.issues) { + patch.issues = []; + } + const issue = { + owner: patchIssue[1], + repo: patchIssue[2], + number: parseInt(patchIssue[4], 10), + url: `https://github.com/${patchIssue[1]}/${patchIssue[2]}/${patchIssue[3]}/${patchIssue[4]}` + } + console.log(`- "${patch.name}" linked to ${issue.url}`); + patch.issues.push(issue); + } + + if (patchIssues.length === 0) { + console.log(`- "${patch.name}" not linked to any issue`); + } + } + patches = patches.filter(patch => patch.issues); + + console.log(); + console.log("Check status of GitHub issues/PR"); + for (const patch of patches) { + for (const issue of patch.issues) { + const response = await octokit.issues.get({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number + }); + issue.state = response?.data?.state ?? "unknown"; + console.log(`- [${issue.state}] ${issue.url}`); + } + } + + console.log(); + console.log("Drop patches when possible"); + patches = patches.filter(patch => patch.issues.every(issue => issue.state === "closed")); + if (patches.length > 0) { + const res = []; + for (const patch of patches) { + console.log(`- drop "${patch.name}"`); + fs.unlinkSync(path.join(rootDir, patch.name)); + res.push(`- \`${patch.name}\` was linked to now closed: ` + + patch.issues.map(issue => `[${issue.owner}/${issue.repo}#${issue.number}](${issue.url})`).join(", ")); + } + return res.join("\n"); + } + else { + console.log("- No patch to drop at this time"); + return ""; + } +} + + +/******************************************************************************* +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 +}); + + +dropPatchesWhenPossible() + .then(res => { + core.exportVariable("dropped_patches", res); + console.log(); + console.log("Set dropped_variables env variable"); + console.log(res); + console.log("== The end =="); + }) + .catch(err => { + console.error(err); + process.exit(1); + }); \ No newline at end of file