Skip to content

Commit 2f15bb4

Browse files
authored
Add "freeze patch" mechanism, add one for filter-effects-1 (#1505)
This creates a new way to curate the results of a crawl: a freeze patch allows to freeze the results of a crawl to a specific commit ID in Webref. This is meant for specs that are temporarily broken beyond repair. First such patch is for the filter-effects-1 spec. Each freeze patch is implemented as a JSON file named after the spec's shortname and that contains the commit ID and a `pending` key that links to the issue that tracks the problem in some GitHub repository. The `clean-patches` script proposes to drop the patch when the issue gets closed.
1 parent a1c73a0 commit 2f15bb4

File tree

7 files changed

+122
-18
lines changed

7 files changed

+122
-18
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ jobs:
77
runs-on: ubuntu-latest
88
steps:
99
- uses: actions/checkout@v4
10+
with:
11+
# Need to checkout all history for curation job
12+
fetch-depth: 0
1013
- uses: actions/setup-node@v4
1114
with:
1215
node-version: 20

ed/freezepatches/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Freeze patches
2+
3+
These are patches applied to specs to freeze all the extracts for that spec to a past result, identified by a commit ID.
4+
5+
Each patch should be a JSON file named after the spec's shortname that defines a JSON object with two keys:
6+
7+
- `commit`: The full commit ID in Webref that identifies the crawl results to use for the spec.
8+
- `pending`: The URL of an issue that tracks the problem. This allows to detect when the patch is no longer needed.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"commit": "647faa1643647b66b31a505e5b247a8695955352",
3+
"pending": "https://github.com/w3c/fxtf-drafts/issues/591"
4+
}

tools/apply-patches.js

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import path from 'node:path';
1919
import util from 'node:util';
2020
import { fileURLToPath } from 'node:url';
2121
import { execFile as execCb } from 'node:child_process';
22-
import { createFolderIfNeeded } from './utils.js';
22+
import { createFolderIfNeeded, loadJSON, getTargetedExtracts } from './utils.js';
2323
const execFile = util.promisify(execCb);
2424

2525
async function applyPatches(rawFolder, outputFolder, type) {
@@ -61,6 +61,7 @@ async function applyPatches(rawFolder, outputFolder, type) {
6161
];
6262

6363
await createFolderIfNeeded(outputFolder);
64+
await applyFreezePatches(rawFolder, outputFolder);
6465

6566
for (const { name, srcDir, dstDir, patchDir, fileExt } of packages) {
6667
if (!type.includes(name)) {
@@ -100,6 +101,66 @@ async function applyPatches(rawFolder, outputFolder, type) {
100101
}
101102

102103

104+
/**
105+
* Apply "freeze" patches, which freeze curation data for a spec to the results
106+
* of a previous crawl result, identified by a commit ID.
107+
*
108+
* Freeze patches are meant to be used for specs that are (hopefully
109+
* temporarily) severely broken.
110+
*/
111+
async function applyFreezePatches(rawFolder, outputFolder) {
112+
const patchDir = path.join(rawFolder, 'freezepatches');
113+
const patchFiles = await fs.readdir(patchDir);
114+
115+
const outputIndex = await loadJSON(path.join(outputFolder, 'index.json'));
116+
let patchApplied = false;
117+
118+
for (const file of patchFiles) {
119+
if (!file.endsWith('.json')) {
120+
continue;
121+
}
122+
123+
const shortname = file.replace(/\.json$/, '');
124+
const patch = path.join(patchDir, file);
125+
const json = await loadJSON(patch);
126+
127+
console.log(`Applying ${path.relative(rawFolder, patch)}`);
128+
const outputSpecPos = outputIndex.results.findIndex(spec => spec.shortname === shortname);
129+
130+
// Get back to the patch commit
131+
// (note this does not touch the `curated` folder because it is in
132+
// the `.gitignore` file)
133+
await execFile('git', ['checkout', json.commit]);
134+
135+
const crawlIndex = await loadJSON(path.join(rawFolder, 'index.json'));
136+
const crawlSpec = crawlIndex.results.find(spec => spec.shortname === shortname);
137+
138+
for (const propValue of Object.values(crawlSpec)) {
139+
const extractFiles = getTargetedExtracts(propValue);
140+
for (const extractFile of extractFiles) {
141+
await fs.copyFile(
142+
path.join(rawFolder, extractFile),
143+
path.join(outputFolder, extractFile)
144+
);
145+
}
146+
outputIndex.results.splice(outputSpecPos, 1, crawlSpec);
147+
}
148+
149+
await execFile('git', ['checkout', 'main']);
150+
patchApplied = true;
151+
}
152+
153+
// Update curated version of the index.json file
154+
if (patchApplied) {
155+
await fs.writeFile(
156+
path.join(outputFolder, 'index.json'),
157+
JSON.stringify(outputIndex, null, 2),
158+
'utf8'
159+
);
160+
}
161+
}
162+
163+
103164
/**************************************************
104165
Export methods for use as module
105166
**************************************************/

tools/clean-patches.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async function dropPatchesWhenPossible() {
3030
if (subDir.endsWith("patches")) {
3131
const files = fs.readdirSync(path.join(rootDir, subDir));
3232
for (const file of files) {
33-
if (file.endsWith(".patch")) {
33+
if (file.endsWith(".patch") || file.endsWith(".json")) {
3434
const patch = path.join(subDir, file);
3535
console.log(`- add "${patch}"`);
3636
patches.push({ name: patch });
@@ -44,9 +44,16 @@ async function dropPatchesWhenPossible() {
4444
const diffStart = /^---$/m;
4545
const issueUrl = /(?<=^|\s)https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/(\d+)(?=\s|$)/g;
4646
for (const patch of patches) {
47+
let patchIssues;
4748
const contents = fs.readFileSync(path.join(rootDir, patch.name), "utf8");
48-
const desc = contents.substring(0, contents.match(diffStart)?.index);
49-
const patchIssues = [...desc.matchAll(issueUrl)];
49+
if (patch.name.endsWith(".json")) {
50+
const json = JSON.parse(contents);
51+
patchIssues = [...(json.pending ?? '').matchAll(issueUrl)];
52+
}
53+
else {
54+
const desc = contents.substring(0, contents.match(diffStart)?.index);
55+
patchIssues = [...desc.matchAll(issueUrl)];
56+
}
5057
for (const patchIssue of patchIssues) {
5158
if (!patch.issues) {
5259
patch.issues = [];

tools/prepare-curated.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import { rimraf } from 'rimraf';
2323
import {
2424
createFolderIfNeeded,
2525
loadJSON,
26-
copyFolder } from './utils.js';
26+
copyFolder,
27+
getTargetedExtracts } from './utils.js';
2728
import { applyPatches } from './apply-patches.js';
2829
import { dropCSSPropertyDuplicates } from './drop-css-property-duplicates.js';
2930
import { curateEvents } from './amend-event-data.js';
@@ -35,10 +36,9 @@ import { crawlSpecs } from 'reffy';
3536
*/
3637
async function removeFromCuration(spec, curatedFolder) {
3738
for (const property of ['cddl', 'css', 'elements', 'events', 'idl']) {
38-
if (spec[property] &&
39-
(typeof spec[property] === 'string') &&
40-
spec[property].match(/^[^\/]+\/[^\/]+\.(json|idl|cddl)$/)) {
41-
const filename = path.join(curatedFolder, spec[property]);
39+
const extractFiles = getTargetedExtracts(spec[property]);
40+
for (const extractFile of extractFiles) {
41+
const filename = path.join(curatedFolder, extractFile);
4242
console.log(`Removing ${spec.standing} ${spec.title} from curation: del ${filename}`);
4343
await fs.unlink(filename);
4444
}
@@ -56,16 +56,15 @@ async function removeFromCuration(spec, curatedFolder) {
5656
async function cleanCrawlOutcome(spec) {
5757
for (const property of Object.keys(spec)) {
5858
// Only consider properties that link to an extract
59-
if (spec[property] &&
60-
(typeof spec[property] === 'string') &&
61-
spec[property].match(/^[^\/]+\/[^\/]+\.(json|idl|cddl)$/)) {
62-
try {
63-
await fs.lstat(path.join(curatedFolder, spec[property]));
64-
}
65-
catch (err) {
66-
delete spec[property];
59+
const extractFiles = getTargetedExtracts(spec[property]);
60+
try {
61+
for (const extractFile of extractFiles) {
62+
await fs.lstat(path.join(curatedFolder, extractFile));
6763
}
6864
}
65+
catch (err) {
66+
delete spec[property];
67+
}
6968
}
7069
}
7170

tools/utils.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,30 @@ async function copyFolder(source, target, { excludeRoot = false } = {}) {
6868
};
6969

7070

71+
/**
72+
* Return the list of extract files that the given value targets.
73+
*
74+
* Note: The `cddl` property value targets an array of extracts, the actual
75+
* extract being under the `file` key each time.
76+
*/
77+
function getTargetedExtracts(value) {
78+
const reExtractFile = /^[^\/]+\/[^\/]+\.(json|idl|cddl)$/;
79+
if (Array.isArray(value)) {
80+
return value
81+
.filter(v => typeof v.file === 'string' && v.file.match(reExtractFile))
82+
.map(v => v.file);
83+
}
84+
else if (typeof value === 'string' && value.match(reExtractFile)) {
85+
return [value];
86+
}
87+
else {
88+
return [];
89+
}
90+
}
91+
7192
export {
7293
createFolderIfNeeded,
7394
loadJSON,
74-
copyFolder
95+
copyFolder,
96+
getTargetedExtracts
7597
};

0 commit comments

Comments
 (0)