Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
21646fa
test: cover validated CodeQL regressions
Wibias Aug 15, 2026
a8684b3
fix: harden project TOML parsing
Wibias Aug 15, 2026
eba1591
fix: harden plugin TOML parsing
Wibias Aug 15, 2026
0e606a5
fix: reject hidden PR comment content
Wibias Aug 15, 2026
2607eb2
ci: run temporary CodeQL repair verification
Wibias Aug 15, 2026
cb1b0a1
test: assert bounded TOML matcher execution
Wibias Aug 15, 2026
98405c9
fix: close validated CodeQL findings [codeql-patch-applied]
github-actions[bot] Aug 15, 2026
69ff030
ci: rerun CodeQL repair verification
Wibias Aug 15, 2026
35feb7d
ci: remove temporary CodeQL repair workflow
Wibias Aug 15, 2026
2a16a6c
test: cover release-note comment handling
Wibias Aug 15, 2026
31c762c
ci: verify final CodeQL release-note fix
Wibias Aug 15, 2026
29e3c3f
ci: correct release-note repair matcher
Wibias Aug 15, 2026
adcd875
fix: ignore non-rendered release-note comments [codeql-patch-applied]
github-actions[bot] Aug 15, 2026
4de66be
ci: remove temporary release-note verifier
Wibias Aug 15, 2026
532d8ad
ci: run final CodeQL fix verification
Wibias Aug 15, 2026
1ff1b90
ci: rerun final CodeQL verification without missing lint script
Wibias Aug 15, 2026
07f166b
ci: remove temporary CodeQL final verifier
Wibias Aug 15, 2026
ca7eadd
ci: relocate CodeQL regressions into owner suites
Wibias Aug 15, 2026
bfa1931
ci: replace CodeQL test relocation verifier
Wibias Aug 15, 2026
2886570
ci: rerun CodeQL test relocation
Wibias Aug 15, 2026
bd08ced
ci: replace CodeQL relocation verifier v2
Wibias Aug 15, 2026
c1a86be
ci: rerun CodeQL test relocation v3
Wibias Aug 15, 2026
4fb6919
ci: replace CodeQL relocation verifier v3
Wibias Aug 15, 2026
d928b3e
ci: rerun CodeQL test relocation v4
Wibias Aug 15, 2026
4aa64ac
test: colocate CodeQL regressions with owner suites [test-relocation-…
github-actions[bot] Aug 15, 2026
84a30a9
ci: remove temporary CodeQL relocation workflow
Wibias Aug 15, 2026
87284f3
ci: verify colocated CodeQL regressions
Wibias Aug 15, 2026
ec80dff
ci: remove temporary final CodeQL verifier
Wibias Aug 15, 2026
9a1e2d8
ci: apply viable CodeRabbit fixes for PR 1750
Wibias Aug 15, 2026
e2714ef
ci: fix PR 1750 CodeRabbit helper workflow
Wibias Aug 15, 2026
d442cf1
fix: address viable CodeRabbit parser findings
github-actions[bot] Aug 15, 2026
fa3e787
ci: verify final CodeRabbit quote fix
Wibias Aug 15, 2026
0edbacb
fix: reject malformed quoted marketplace values
github-actions[bot] Aug 15, 2026
e689acb
fix(quality-gates): strip HTML comments outside code, not through it
lidge-jun Aug 16, 2026
bd88624
fix(issue-quality): replace the regex code-masker with a linear scanner
lidge-jun Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 120 additions & 3 deletions .github/scripts/issue-quality-core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function unwrapSingleEnclosingFence(text) {
*/
function normalizeRawSectionValue(raw) {
if (typeof raw !== "string") return null;
let value = raw.replace(/<!--[\s\S]*?-->/g, "").trim();
let value = raw.replace(/<!--[\s\S]*?(?:-->|$)/g, "").trim();
if (!value) return null;

// A lone fenced block whose entire body is a stand-in is still a stand-in
Expand Down Expand Up @@ -179,7 +179,7 @@ function stripHtmlMedia(text) {
if (typeof text !== "string") return "";
let s = text
.replace(/<img\b[^>]*>/gi, " ")
.replace(/<!--[\s\S]*?-->/g, " ");
.replace(/<!--[\s\S]*?(?:-->|$)/g, " ");

// Whole media blocks: replace only when the inner content is not
// substantive text (no word characters outside tags).
Expand Down Expand Up @@ -419,12 +419,129 @@ function isMediaOnly(text) {
return stripped.replace(/\s+/g, "").length === 0;
}

/**
* Strip HTML comments, placeholder-only values, and trim whitespace.
*/
function stripHtmlCommentsOutsideCode(raw) {
const text = String(raw);
// Linear scanner, deliberately not a regex. The previous masker combined a
// variable-length delimiter capture, a lazy whole-input scan and a
// backreference, which backtracks catastrophically on adversarial input: a
// 60k-character issue body took ~10.5s, inside an automation trust boundary
// that anyone can post to. This walks the string once.
//
// It also fixes two GFM cases the regex got wrong: a closing fence may be
// LONGER than its opener, and a code span may contain a line ending.
let out = "";
let i = 0;
let atLineStart = true;

const lineEnd = (from) => {
const nl = text.indexOf("\n", from);
return nl === -1 ? text.length : nl;
};

while (i < text.length) {
const ch = text[i];

// Fenced block: at most three leading spaces, then >= 3 backticks or tildes.
if (atLineStart && (ch === "`" || ch === "~" || ch === " " || ch === "\t")) {
let j = i;
let indent = 0;
while (j < text.length && (text[j] === " " || text[j] === "\t") && indent < 4) { j++; indent++; }
const marker = text[j];
if (indent < 4 && (marker === "`" || marker === "~")) {
let run = 0;
while (text[j + run] === marker) run++;
if (run >= 3) {
// Copy the opening line verbatim, then everything up to a closing
// fence of the SAME character and AT LEAST the same length.
// `cursor` is the index of the newline ending the current line, so the
// next line starts at cursor + 1.
let cursor = lineEnd(j + run);
let closed = false;
while (cursor < text.length) {
const start = cursor + 1;
let p = start;
let ind = 0;
while (p < text.length && (text[p] === " " || text[p] === "\t") && ind < 4) { p++; ind++; }
let closeRun = 0;
while (text[p + closeRun] === marker) closeRun++;
const after = p + closeRun;
const rest = text.slice(after, lineEnd(after));
if (ind < 4 && closeRun >= run && rest.trim() === "") {
const end = lineEnd(after);
out += text.slice(i, end);
i = end;
closed = true;
break;
}
const next = lineEnd(start);
if (next <= cursor) break;
cursor = next;
}
if (closed) { atLineStart = true; continue; }
// Unclosed fence runs to end of input, per GFM.
out += text.slice(i);
return out;
}
}
}

// Inline code span: a run of N backticks closed by the next run of exactly N.
if (ch === "`") {
let run = 0;
while (text[i + run] === "`") run++;
let p = i + run;
let close = -1;
while (p < text.length) {
if (text[p] === "`") {
let r = 0;
while (text[p + r] === "`") r++;
if (r === run) { close = p + r; break; }
p += r;
continue;
}
p++;
}
if (close !== -1) {
out += text.slice(i, close);
atLineStart = false;
i = close;
continue;
}
}

// HTML comment outside any code region. An unclosed one runs to EOF.
if (ch === "<" && text.startsWith("<!--", i)) {
const end = text.indexOf("-->", i + 4);
if (end === -1) return out;
i = end + 3;
continue;
}

out += ch;
atLineStart = ch === "\n";
i++;
}
return out;
}

/**
* Strip HTML comments, placeholder-only values, and trim whitespace.
*/
/**
* Strip HTML comments, placeholder-only values, and trim whitespace.
*/
function clean(raw) {
if (typeof raw !== "string") return "";
let s = raw.replace(/<!--[\s\S]*?-->/g, "");
// Comment stripping must not reach inside fenced code. GFM treats fence
// contents as literal text, so a `<!--` in a code sample never opens a
// comment; letting an unclosed one run through EOF swallowed the rest of the
// section and rejected valid issues as "too vague to act on". Fence contents
// are preserved, not deleted -- they are legitimate reproduction evidence
// that emptiness and duplicate detection still need to see.
let s = stripHtmlCommentsOutsideCode(raw);
// Media-only sections (a lone screenshot or embed) carry no reportable
// text. Strip the media tokens so the section participates in emptiness and
// duplicate detection like any other blank section. This closes the
Expand Down
49 changes: 49 additions & 0 deletions .github/scripts/issue-quality.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,8 @@ describe("normalisation", () => {

it("strips HTML comments", () => {
assert.equal(clean("Hello <!-- hidden --> world"), "Hello world");
assert.equal(clean("<!--\nhidden issue text"), "");
assert.equal(clean("<!-- hidden -->\nVisible text"), "Visible text");
});

it("normalises punctuation and capitalisation", () => {
Expand Down Expand Up @@ -2092,3 +2094,50 @@ describe("detectAreaLabels", () => {
assert.ok(labels.includes("streaming"), `got ${labels.join(",")}`);
});
});

describe("clean() respects fenced code (regression)", () => {
it("keeps section text that follows a comment-like literal in a fence", () => {
// A `<!--` inside a code sample is literal text under GFM. Stripping
// comments before fences let it run to EOF and swallow the rest of the
// section, so a valid issue was rejected as too vague to act on.
const goal = [
"```html",
"<!-- literal unclosed-comment example",
"```",
"",
"The provider catalog fails to load on startup and blocks routing.",
].join("\n");

assert.ok(clean(goal).includes("provider catalog fails to load"));
});

it("still strips a real HTML comment outside code", () => {
assert.equal(clean("<!-- hidden -->").trim(), "");
});
});

describe("code-region scanning is GFM-correct and linear (regression)", () => {
it("honors a closing fence longer than its opener", () => {
// GFM allows the closing fence to be longer. Requiring an exact-length
// match left the block unterminated, so the comment inside it ran to EOF
// and swallowed the visible section below.
const goal = ["```html", "<!-- literal example", "````", "", "The catalog fails to load."].join("\n");
assert.ok(clean(goal).includes("The catalog fails to load."));
});

it("honors a code span containing a line ending", () => {
const goal = ["`first", "second`", "", "The catalog fails to load."].join("\n");
assert.ok(clean(goal).includes("The catalog fails to load."));
});

it("stays linear on adversarial input", () => {
// The previous masker combined a variable-length delimiter capture, a lazy
// whole-input scan and a backreference. A 60k-character body took ~10.5s
// inside an automation trust boundary anyone can post to.
const started = Date.now();
clean("```html\n" + "x".repeat(60000) + "\n");
clean("`a`".repeat(20000));
const elapsed = Date.now() - started;
assert.ok(elapsed < 2000, `code-region scan took ${elapsed}ms; expected a linear scan`);
});
});
14 changes: 10 additions & 4 deletions .github/scripts/pr-quality.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ const PR_TEMPLATE_BOILERPLATE_LINES = new Set([

/** Case-insensitive whole-word match for the GUI surface (repo convention: `gui/`). */
const GUI_CUE_RE = /\bgui\b/i;
/** HTML comments, which GitHub never renders. */
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
/** HTML comments, which GitHub never renders. An unclosed comment runs through EOF. */
const HTML_COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g;
/** Fenced code blocks (``` or ~~~) whose content GitHub does not render. */
const FENCED_CODE_RE = /(?:^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1[ \t]*(?=\n|$)/gm;
/** Embedded markdown image (`![alt](url)`), as GitHub renders for dropped images. */
Expand Down Expand Up @@ -147,7 +147,7 @@ function assessPrDescription(body) {
const withoutTemplate = stripPrTemplateBoilerplate(withoutReadiness);
const cleaned = clean(withoutTemplate);
if (!cleaned) {
const strippedComments = withoutTemplate.replace(/<!--[\s\S]*?-->/g, "").trim();
const strippedComments = withoutTemplate.replace(HTML_COMMENT_RE, "").trim();
if (!strippedComments) return { ok: false, reason: "empty" };
if (isPlaceholderOnlyValue(strippedComments)) {
return { ok: false, reason: "placeholder" };
Expand Down Expand Up @@ -245,7 +245,13 @@ function hasGuiOverride({ comments = [] }) {
* fenced code blocks. Image syntax there is literal text, not evidence.
*/
function stripNonRenderedRegions(body) {
return body.replace(HTML_COMMENT_RE, "").replace(FENCED_CODE_RE, "");
// Fenced code MUST be removed first. GFM treats fence contents as literal
// text, so a `<!--` inside a fence never opens an HTML comment. Stripping
// comments first let an unclosed comment-like literal in a code sample run
// through EOF and swallow the real body after it, which rejected valid
// descriptions: a GUI PR whose screenshot followed such an example lost its
// evidence, and an issue lost the sections it was validated on.
return body.replace(FENCED_CODE_RE, "").replace(HTML_COMMENT_RE, "");
}

/**
Expand Down
33 changes: 33 additions & 0 deletions .github/scripts/pr-quality.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ describe("assessPrDescription", () => {
assessPrDescription("<!-- release notes by coderabbit.ai -->\n\n<!-- end -->").reason,
"empty",
);
assert.equal(
assessPrDescription("<!--\n![proof](https://example.invalid/screenshot.png)").reason,
"empty",
);
});

it("rejects placeholder-only bodies", () => {
Expand Down Expand Up @@ -291,6 +295,10 @@ describe("hasScreenshotEvidence", () => {
hasScreenshotEvidence('<!-- <img src="https://example.com/ui.png"> -->'),
false,
);
assert.equal(
hasScreenshotEvidence("<!--\n![after](https://example.com/after.png)"),
false,
);
});

it("rejects img tags without a renderable src and references without a definition", () => {
Expand Down Expand Up @@ -1068,3 +1076,28 @@ describe("collectPrQualityFailures", () => {
assert.ok(!failures.some((f) => f.code === "missing_ui_screenshot"));
});
});

describe("comment stripping respects fenced code (regression)", () => {
it("keeps a screenshot that follows a comment-like literal in a fence", () => {
// GFM treats fence contents as literal text, so `<!--` inside a code sample
// never opens an HTML comment. Stripping comments before fences let the
// unclosed literal run to EOF and swallow the real screenshot below it,
// rejecting a valid GUI PR.
const body = [
"Example of a raw HTML comment:",
"",
"```html",
"<!-- literal unclosed-comment example",
"```",
"",
"![after](https://example.invalid/after.png)",
].join("\n");

assert.equal(hasScreenshotEvidence(body), true);
});

it("still ignores a screenshot inside a real HTML comment", () => {
const body = ["<!--", "![hidden](https://example.invalid/hidden.png)", "-->"].join("\n");
assert.equal(hasScreenshotEvidence(body), false);
});
});
2 changes: 1 addition & 1 deletion scripts/release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ export function stripCarriedReleaseNotes(body: string): string {
export function isEmptyGeneratedNotes(body: string): boolean {
const withoutComment = body
.replace(/\r\n/g, "\n")
.replace(/<!--[\s\S]*?(?:-->|$)/g, "")
.split("\n")
.filter(line => !/^<!--.*-->$/.test(line.trim()))
.filter(line => !/^\*\*Full Changelog\*\*:/.test(line))
.join("\n");
return !hasNonWhitespace(withoutComment);
Expand Down
6 changes: 3 additions & 3 deletions src/codex/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ function stripRootRoutedModel(content: string): string {
.filter((line, i) => {
const isRoot = firstTable === -1 || i < firstTable;
if (!isRoot) return true;
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
if (!m) return true;
const model = parseTomlString(m[1]);
return !model?.includes("/");
Expand Down Expand Up @@ -485,7 +485,7 @@ function setRootModelCatalogPath(content: string, catalogPath: string): string {
const rootEnd = firstTable === -1 ? lines.length : firstTable;
for (let i = 0; i < rootEnd; i++) {
const m = lines[i].match(
/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/,
/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/,
);
if (!m) continue;
const existing = parseTomlString(m[1]);
Expand Down Expand Up @@ -580,7 +580,7 @@ function stripOpencodexCatalogPath(content: string): string {
.split("\n")
.filter((line) => {
const m = line.match(
/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/,
/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/,
);
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
})
Expand Down
2 changes: 1 addition & 1 deletion src/codex/plugins-doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function readMarketplaceTable(configText: string, name: string): Record<string,
for (let i = start; i < lines.length; i++) {
const line = lines[i] ?? "";
if (/^\s*\[/.test(line)) break; // next table starts; stop
const m = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*("(?:\\.|[^"])*"|'[^']*'|[^#]+?)\s*(?:#.*)?$/);
const m = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*("(?:\\.|[^"\\])*"|'[^']*'|(?!["'])[^\s#]+)\s*(?:#.*)?$/);
if (!m) continue;
table[m[1]] = unquoteTomlValue(m[2].trim());
}
Expand Down
4 changes: 2 additions & 2 deletions src/codex/project-config-warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function parseTomlDocument(content: string): TomlDocument {
current = section;
continue;
}
const kv = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*("(?:\\.|[^"])*"|'[^']*'|[^\s#]+)\s*(?:#.*)?$/);
const kv = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*("(?:\\.|[^"\\])*"|'[^']*'|[^\s#]+)\s*(?:#.*)?$/);
if (kv) current[kv[1]!] = parseTomlString(kv[2]!);
}

Expand Down Expand Up @@ -422,4 +422,4 @@ export function printProjectCodexConfigWarnings(
}
}
return warnings;
}
}
13 changes: 13 additions & 0 deletions tests/codex-inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ describe("Codex config injection", () => {
expect(stripped).toContain('model_verbosity = "high"');
});

test("malformed quoted root values cannot wedge restore transforms", () => {
const slashRun = "\\".repeat(64);
const stripped = stripOpencodexConfig([
'model_provider = "opencodex"',
`model = "${slashRun}`,
`model_catalog_json = "${slashRun}`,
"",
].join("\n"));

expect(stripped).toContain(`model = "${slashRun}`);
expect(stripped).toContain(`model_catalog_json = "${slashRun}`);
}, 2_000);

test("preserves non-opencodex routed model names during fallback restore", () => {
const stripped = stripOpencodexConfig([
'model_provider = "proxy"',
Expand Down
Loading
Loading