fix: round 4 of the release review of 6.3.0
Round 4 (v6.2.0..41ea8fc, WARN: 9 MAJOR, 3 MINOR); the nine MAJOR fixed, each behind a test that failed first (operator 2026-10-10). Three were regressions from round 3's shared Files: reader. - fieldPaths keeps a backticked path with punctuation after it and one inside a note, a directory (docs/) and a glob (src/*.mjs) in plain text, and app/(auth)/x.ts whole; a bare word counts only alone and capitalised (Makefile), so "format" or "v6.3.0" in a description is no path. The K6 sweep plans what v6.2.0 planned for `a.mjs`: and `b.mjs`. - skill frontmatter: a quoted value may carry a YAML comment after it. - criteria runner: no word with a leading ~ (sh expands it), and nothing after `make test` (another target would run). - release-check: scope_sha_end must be a commit sha (a ref name passed coverage whatever was reviewed), and README lines changed after the review may differ only in the badge's version. The round-3 leading-zero example is 7 characters now, the shortest sha accepted. The three MINOR (one word screen for git, the exec copy in regression-check, a shared isMain helper) are not in this round. Suite 2175 (2173/0/2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
41ea8fc317
commit
ab6dcd838e
6 changed files with 137 additions and 33 deletions
|
|
@ -286,15 +286,20 @@ export function onFailureAction(text) {
|
|||
|
||||
/**
|
||||
* The paths a field names, in order. A backticked span is a path as written,
|
||||
* parentheses and all (`app/(auth)/x.sh`). In the plain text around the spans
|
||||
* a parenthesised note is not a path ("(new: covers x, y)"), and every word
|
||||
* that looks like a path is one ("tests/a.test.mjs + src/b.mjs" names both);
|
||||
* an entry (comma- or line-separated) with no such word gives its first word,
|
||||
* so a bare `Makefile` is still a path. Release review of 6.3.0, rounds 1-2.
|
||||
* parentheses and all (`app/(auth)/x.sh`), also with punctuation after it
|
||||
* (`a.mjs`.) and inside a note ("(fixture: `t/a.json`)"). In the plain text a
|
||||
* parenthesised note with no span in it is not a path ("(new: covers x, y)"),
|
||||
* and every word that looks like a path is one: a name with an extension, a
|
||||
* path with a slash, a directory (`docs/`) or a glob (`src/*.mjs`). A word
|
||||
* with no slash and no extension counts only alone in its entry and
|
||||
* capitalised, like `Makefile`; "format" or "v6.3.0" in a description is no
|
||||
* path. Release review of 6.3.0, rounds 1-4.
|
||||
*/
|
||||
const PATH_WORD = /^(?:[\w@.-]+\/)+[\w@.-]+$|^[\w@-][\w@.-]*\.[A-Za-z0-9]+$/;
|
||||
const SEG = '[\\w@.*?()-]';
|
||||
const PATH_WORD = new RegExp(`^(?:${SEG}+\\/)+${SEG}*$|^[\\w@*?-][\\w@.*?-]*\\.[A-Za-z*][A-Za-z0-9*]*$`);
|
||||
// Abbreviations that PATH_WORD would read as a name with an extension.
|
||||
const NOT_A_PATH = /^(?:e\.g|i\.e|n\.b|cf|etc|vs)$/i;
|
||||
const BARE_NAME = /^[A-Z][\w.-]*$/;
|
||||
const dotSlash = (p) => p.replace(/^\.\//, '');
|
||||
/**
|
||||
* The one reader of a step's `Files:` field: this file's tests-only check and
|
||||
|
|
@ -303,21 +308,23 @@ const dotSlash = (p) => p.replace(/^\.\//, '');
|
|||
*/
|
||||
export function fieldPaths(text) {
|
||||
const spans = [];
|
||||
const masked = String(text ?? '').replace(/`([^`\n]+)`/g, (_, p) => `\u0000${spans.push(p.trim()) - 1}\u0000`);
|
||||
const masked = String(text ?? '')
|
||||
.replace(/`([^`\n]+)`/g, (_, p) => `\u0000${spans.push(p.trim()) - 1}\u0000`)
|
||||
.replace(/(^|\s)\(([^()\n]*)\)/g, (m, pre, inner) => (inner.includes('\u0000') ? m : `${pre} `));
|
||||
const out = [];
|
||||
for (const raw of masked.replace(/\([^()\n]*\)/g, ' ').split(/[,\n]/)) {
|
||||
for (const raw of masked.split(/[,\n]/)) {
|
||||
const entry = raw.trim().replace(/^[-*]\s+/, '');
|
||||
if (entry === '') continue;
|
||||
const words = entry.split(/\s+/);
|
||||
const found = [];
|
||||
for (const word of entry.split(/\s+/)) {
|
||||
const span = /^\u0000(\d+)\u0000$/.exec(word);
|
||||
for (const word of words) {
|
||||
const w = word.replace(/^[(\[]+/, '').replace(/[.,;:)\]]+$/, '');
|
||||
const span = /^\u0000(\d+)\u0000$/.exec(w);
|
||||
if (span) found.push(spans[Number(span[1])]);
|
||||
else {
|
||||
const bare = word.replace(/[.;:]+$/, '');
|
||||
if (PATH_WORD.test(bare) && !NOT_A_PATH.test(bare)) found.push(bare);
|
||||
}
|
||||
else if (PATH_WORD.test(w) && !NOT_A_PATH.test(w)) found.push(w);
|
||||
}
|
||||
out.push(...(found.length > 0 ? found : [entry.split(/\s+/)[0]]));
|
||||
if (found.length === 0 && words.length === 1 && BARE_NAME.test(words[0])) found.push(words[0]);
|
||||
out.push(...found);
|
||||
}
|
||||
return out.filter((p) => p && !p.includes('\u0000')).map(dotSlash);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,18 +39,26 @@ export function plainScalarHazards(value) {
|
|||
return out;
|
||||
}
|
||||
|
||||
// A quoted value ends at its closing quote; after it only a YAML comment may
|
||||
// follow (`name: "x" # pinned`). Inside double quotes a backslash escapes the
|
||||
// next character; inside single quotes '' is one quote.
|
||||
function unquote(v) {
|
||||
if (v.startsWith('"')) {
|
||||
// The last quote closes the value unless an odd run of backslashes escapes it.
|
||||
const escaped = (v.slice(1, -1).match(/\\*$/)[0].length % 2) === 1;
|
||||
if (v.length < 2 || !v.endsWith('"') || escaped) return { error: 'unclosed double quote' };
|
||||
try { return { value: JSON.parse(v) }; } catch { return { error: 'bad escape in a double-quoted value' }; }
|
||||
const q = v[0];
|
||||
if (q !== '"' && q !== "'") return null;
|
||||
let end = -1;
|
||||
for (let i = 1; i < v.length; i++) {
|
||||
if (q === '"' && v[i] === '\\') { i++; continue; }
|
||||
if (v[i] !== q) continue;
|
||||
if (q === "'" && v[i + 1] === "'") { i++; continue; }
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
if (v.startsWith("'")) {
|
||||
if (v.length < 2 || !v.endsWith("'")) return { error: 'unclosed single quote' };
|
||||
return { value: v.slice(1, -1).replace(/''/g, "'") };
|
||||
}
|
||||
return null;
|
||||
if (end < 0) return { error: q === '"' ? 'unclosed double quote' : 'unclosed single quote' };
|
||||
const rest = v.slice(end + 1);
|
||||
if (rest !== '' && !/^\s+#/.test(rest)) return { error: 'text after the closing quote that is not a comment' };
|
||||
const body = v.slice(0, end + 1);
|
||||
if (q === "'") return { value: body.slice(1, -1).replace(/''/g, "'") };
|
||||
try { return { value: JSON.parse(body) }; } catch { return { error: 'bad escape in a double-quoted value' }; }
|
||||
}
|
||||
|
||||
function blockValue(lines, style, chomp) {
|
||||
|
|
|
|||
|
|
@ -295,8 +295,9 @@ const PLAIN_WORD = /^(?:[A-Za-z0-9_@%^~:.,=+/-]+|[A-Za-z0-9_./][A-Za-z0-9_@%^~:.
|
|||
// `only` the ones whose value must be one of a few words: a built-in node test
|
||||
// reporter only formats, a reporter path would load code),
|
||||
// whether `--` may hand the rest to the repo's own script (`dashDash`: then
|
||||
// only paths follow), and whether a plain word may carry `=` (make reads
|
||||
// `VAR=value` as an override). The lists hold options that only read.
|
||||
// only paths follow), and whether nothing may follow the fixed words at all
|
||||
// (`noWords`: make reads a word as one more target, or `VAR=value` as an
|
||||
// override). The lists hold options that only read.
|
||||
const RUNNER_ARGS = {
|
||||
npm: { fixed: (t) => (t[1] === 'run' ? 3 : 2), opts: [], dashDash: true },
|
||||
node: {
|
||||
|
|
@ -310,7 +311,7 @@ const RUNNER_ARGS = {
|
|||
pytest: { fixed: 1, opts: ['-q', '-qq', '-v', '-vv', '-x', '-s', '--quiet', '--verbose', '--exitfirst', '--tb', '--maxfail'] },
|
||||
cargo: { fixed: 2, opts: ['--release', '--workspace', '--all', '--lib', '--doc', '-q', '--quiet', '--no-fail-fast'] },
|
||||
go: { fixed: 2, opts: ['-v', '-short', '-race', '-failfast', '-cover', '-count', '-run'], value: ['-count', '-run'] },
|
||||
make: { fixed: 2, opts: [], noAssign: true },
|
||||
make: { fixed: 2, opts: [], noWords: true },
|
||||
};
|
||||
RUNNER_ARGS.python = RUNNER_ARGS.python3 = RUNNER_ARGS.uv = { ...RUNNER_ARGS.pytest, fixed: 3 };
|
||||
|
||||
|
|
@ -318,6 +319,8 @@ function screenArgs(t, spec) {
|
|||
const fixed = typeof spec.fixed === 'function' ? spec.fixed(t) : spec.fixed;
|
||||
for (let i = fixed, paths = false; i < t.length; i++) {
|
||||
const w = t[i];
|
||||
// `make test install` runs the install target too: make takes nothing more.
|
||||
if (spec.noWords) return deny(`\`${t.slice(0, fixed).join(' ')}\` with more after it (${w})`);
|
||||
if (paths) {
|
||||
if (w.startsWith('-')) return deny(`an option after \`--\` (${w}): only paths may follow it`);
|
||||
continue;
|
||||
|
|
@ -334,7 +337,6 @@ function screenArgs(t, spec) {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (spec.noAssign && w.includes('=')) return deny(`\`${t[0]}\` with a variable override (${w})`);
|
||||
}
|
||||
return permit();
|
||||
}
|
||||
|
|
@ -431,6 +433,10 @@ function screenShape(command, opts, writeTime) {
|
|||
|
||||
const tokens = text.trim().split(/\s+/);
|
||||
if (!Object.hasOwn(RUNNERS, tokens[0])) return deny(`\`${tokens[0]}\` is not a known test runner`);
|
||||
// /bin/sh expands a tilde at the start of a word (and after = or : in an
|
||||
// assignment), so such a word is not one the shell passes on unchanged.
|
||||
const tilde = tokens.find((w) => /(^|[=:])~/.test(w));
|
||||
if (tilde !== undefined) return deny(`a word the shell would rewrite (${tilde}): a leading ~ is expanded`);
|
||||
const form = RUNNERS[tokens[0]](tokens, cwd, writeTime);
|
||||
if (!form.allowed || tokens[0] === 'git') return form;
|
||||
const odd = tokens.find((w) => !PLAIN_WORD.test(w));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue