Skip to content

Commit

Permalink
Add workflow that checks patch issues/PRs (w3c#255)
Browse files Browse the repository at this point in the history
* Add workflow that checks patch issues/PRs

This creates an additional workflow that runs once per week, extracts the list
of GitHub issues and PR referenced by patches, checks the status of these issues
and PRs, and creates a PR that drops patches that target now closed issues and
PRs.

Fixes w3c#215.
  • Loading branch information
tidoust authored Jun 2, 2021
1 parent 14ed9db commit 53f4021
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/clean-patches.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
134 changes: 134 additions & 0 deletions tools/clean-patches.js
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 53f4021

Please sign in to comment.