Skip to content

ci: add the deterministic PR hygiene gate (extracted from #903) - #918

Merged
lidge-jun merged 6 commits into
devfrom
codex/pr-hygiene-gate
Aug 3, 2026
Merged

ci: add the deterministic PR hygiene gate (extracted from #903)#918
lidge-jun merged 6 commits into
devfrom
codex/pr-hygiene-gate

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Extraction of @Wibias's deterministic PR hygiene gate (#903) onto dev, with
authorship preserved on all three original commits, plus one tuning commit.

#903 sits fourth in a five-PR stack (#900 → #901 → #902 → #903 → #905), but its
files are self-contained — .github/scripts/pr-hygiene.cjs, its test, its
workflow, and its spec touch nothing the other four add. The rest of that stack
is held on questions that have nothing to do with this gate (see the review
comments on #900, #901, #902, #905), and it should not wait behind them.

Why this one first

Every check here is deterministic: a test-change requirement for behavior paths,
new suppression comments, focused/skipped tests, empty catches, generated output,
and orphaned lockfiles. A contributor who trips one can read the failure and know
exactly what to change. That is the property I was unwilling to trade away in
#901, where CodeRabbit is configured to request changes on "technically credible"
reasons and a readiness reconciler then auto-drafts the PR.

The tuning commit

The gate asked for a regression test whenever a path under a behavior prefix
appeared in the diff. This repository asks for dense explanatory comments in
exactly those files, so a PR that only sharpens one would fail — and the only
escape was a maintainer applying test-exception-approved. That trains
contributors to ask for the label instead of writing tests, which weakens the
gate everywhere it actually matters.

A file whose patch contains only comment or blank lines, added and removed sides
both, no longer counts as a behavior change. One non-comment line anywhere in
that file's patch makes it behavior again, so code cannot hide behind a comment.

Two other worries I raised in review turned out to be unfounded, and I checked
before changing anything:

concern verdict
test.skipIf(...) flagged as a focused test No — the pattern matches .only( / .skip( only
catch { /* best-effort */ } flagged as empty No — the pattern requires a genuinely empty body. Across the whole tree it flags six files, all minified

Verification

  • node --test .github/scripts/pr-hygiene.test.cjs: 19 pass, 0 fail (17 from
    stack 4/5: add deterministic anti-slop CI #903, 2 added here)
  • The new exemption was driven red by removing it
  • Scenario-checked against real shapes from this repo: src-with-test passes,
    src-without-test still fails, comment-only passes, comment-plus-one-code-line
    still fails

Attribution

Commits 1–3 are @Wibias's, unmodified, from agent/pr-hygiene-gate. #903 can be
closed once this lands, or kept open if he would rather land it himself from the
restacked branch — his call, and I have said so on the PR.

Summary by CodeRabbit

  • New Features

    • Added automated pull request quality checks for untested behavior changes, skipped or focused tests, empty error handlers, generated-output changes, lockfile churn, and new suppressions.
    • Pull requests now receive clear status labels and an automated report comment when checks pass or fail.
    • Approved exceptions are supported for eligible checks and are revoked when new commits are added.
  • Documentation

    • Added a specification describing the checks, exceptions, and enforcement rules.

Wibias and others added 4 commits August 3, 2026 11:48
…e change

The gate asks for a test whenever a path under a behavior prefix appears in the
diff. A PR that only rewrites a comment inside `src/` changed no behavior, and
this repository asks for dense explanatory comments in exactly those files — so
the common case of sharpening one would fail the gate, and the only way out
would be a maintainer applying `test-exception-approved`. That is the worst
outcome available: it teaches contributors to request the label instead of
writing tests, which weakens the gate everywhere it actually matters.

A file whose patch contains only comment or blank lines, on both the added and
removed sides, no longer counts as a behavior change. Deliberately narrow: one
non-comment line anywhere in that file's patch makes it behavior again, so a
code change cannot hide behind a comment. Block-comment continuations are
recognized only in the leading-asterisk form; anything more clever reads as code
and keeps the requirement.

Verified against the two shapes I was actually worried about before writing this:
`test.skipIf(...)` and `describe.skipIf(...)` already pass the focused-test rule
(it matches `.only(`/`.skip(` only), and `catch { /* ... */ }` already passes the
empty-catch rule — across the whole tree that rule flags six files, all of them
minified. Neither needed a change.
@github-actions github-actions Bot added the chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature). label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lidge-jun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 159a9a1f-57f1-4d80-9d3a-28bf2e9ed48a

📥 Commits

Reviewing files that changed from the base of the PR and between 4d381c6 and cc5c06c.

📒 Files selected for processing (3)
  • .github/scripts/pr-hygiene.cjs
  • .github/scripts/pr-hygiene.test.cjs
  • .github/workflows/issue-quality-tests.yml
📝 Walkthrough

Walkthrough

The PR adds a patch-based hygiene assessor, a test suite, and a trusted pull_request_target workflow. The workflow analyzes changed files and labels, manages approval exceptions, updates PR status comments, and blocks failing checks.

Changes

PR hygiene enforcement

Layer / File(s) Summary
Patch classification and hygiene rules
.github/scripts/pr-hygiene.cjs
Adds patch parsing, rename-aware path classification, comment-only detection, empty-catch detection, regression-test checks, generated-output checks, lockfile checks, suppression checks, and focused-test checks.
Assessor validation
.github/scripts/pr-hygiene.test.cjs
Tests patch parsing, hygiene failures, deleted-file handling, renames, comment-only changes, generated files, lockfiles, focused tests, empty catches, and approved exceptions.
Trusted workflow enforcement
.github/workflows/pr-hygiene.yml, docs/superpowers/specs/2026-08-02-pr-hygiene-design.md
Adds trusted-branch script loading, GitHub API patch analysis, exception-label invalidation, blocked-label management, bot comments, failure reporting, and the design specification for these checks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PR as Pull request event
  participant Workflow as pr-hygiene.yml
  participant GitHub as GitHub API
  participant Assessor as pr-hygiene.cjs
  PR->>Workflow: Trigger hygiene check
  Workflow->>GitHub: Retrieve changed files and labels
  Workflow->>Assessor: Evaluate trusted patch data
  Assessor-->>Workflow: Return hygiene result
  Workflow->>GitHub: Update labels and bot comment
  Workflow-->>PR: Pass or fail workflow
Loading

Possibly related PRs

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a deterministic PR hygiene gate to CI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/pr-hygiene-gate

Comment @coderabbitai help to get the list of available commands.

`pr-hygiene.test.cjs` shipped with the gate but nothing executed it. Its
workflow only evaluates pull requests, and Cross-platform CI's `paths:` filter
does not match `.github/scripts/**`, so a change that broke the gate's logic
would have merged with the suite green — the exact "skipped is not passed" shape
this repository already guards elsewhere.

This repo already has the right home for it: `issue-quality-tests.yml` runs the
policy-script tests and triggers on their own paths. The hygiene script, its
test, and its workflow join that list on both the pull-request and push
triggers, and the test runs beside the other six.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/pr-hygiene.cjs:
- Around line 34-44: Update resultLines and the hasEmptyCatch text assembly to
preserve diff-hunk boundaries: split on /^@@ .* @@/ and insert a sentinel
between hunks that cannot be consumed by the empty-catch regex’s whitespace
matching. Ensure catch blocks from separate hunks cannot become adjacent during
scanning, including the joined text path used by hasDeletions.
- Around line 149-155: Update the orphan_lockfile condition in the PR hygiene
checks to ignore bun.lock when it appears only in removedFilenames, and use
allPaths rather than filenames when checking for package.json so renames are
recognized. Preserve the existing dependency-change-approved exemption and
failure code.

In @.github/workflows/pr-hygiene.yml:
- Around line 54-67: Replace the per-label getLabel calls in ensureLabel and the
Object.keys(labelDefinitions) loop with one issues.listLabelsForRepo request to
fetch existing labels, then diff the returned names locally and call createLabel
only for missing definitions. Preserve the existing 422 handling for concurrent
label creation and propagate other errors.
- Around line 126-143: Sanitize untrusted filenames in the `failures.map`
formatting before embedding them in the bot comment: neutralize or remove
backticks from each `p` value before the inline-code interpolation, while
preserving the existing path list and failure message structure.
- Around line 3-5: Update the pr-hygiene workflow’s label-revocation logic in
assessHygiene to run for reopened pull requests as well as synchronize events,
ensuring close→push→reopen cycles revoke stale exception labels before hygiene
assessment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5c202c28-60b3-49fb-834c-e632854d43fc

📥 Commits

Reviewing files that changed from the base of the PR and between 25c2477 and 4d381c6.

📒 Files selected for processing (4)
  • .github/scripts/pr-hygiene.cjs
  • .github/scripts/pr-hygiene.test.cjs
  • .github/workflows/pr-hygiene.yml
  • docs/superpowers/specs/2026-08-02-pr-hygiene-design.md

Comment thread .github/scripts/pr-hygiene.cjs Outdated
Comment thread .github/scripts/pr-hygiene.cjs
Comment on lines +3 to +5
on:
pull_request_target:
types: [opened, reopened, synchronize, labeled, unlabeled]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does GitHub Actions pull_request_target fire a synchronize event for commits pushed to a pull request branch while the pull request is closed, and does reopening later re-trigger synchronize or only reopened?

💡 Result:

The pull_request_target event does not trigger for a synchronize activity when a pull request is closed [1]. The synchronize event type is specifically associated with updates to an active (open) pull request's source branch [2][3]. When a pull request is closed, it is no longer in a state that triggers the synchronize activity [1]. Consequently, pushing new commits to the pull request branch while the pull request is closed will not fire a pull_request_target workflow with the synchronize activity [1]. Regarding your second question, reopening a closed pull request triggers a reopened activity type, not a synchronize activity [1]. If you require workflows to run upon the reopening of a pull request, you must explicitly include the reopened activity type in your workflow configuration: on: pull_request_target: types: [opened, synchronize, reopened] If a pull request is reopened, the pull_request_target event will fire with the reopened action, but it will not retrospectively trigger synchronize events for any commits pushed while the pull request was closed [1]. To ensure that workflows process the latest state after a pull request is reopened, you would need to trigger a job based on the reopened event [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
file=".github/workflows/pr-hygiene.yml"
printf '%s\n' "=== workflow lines 1-120 ==="
cat -n "$file" | sed -n '1,120p'
printf '%s\n' "=== related exception references ==="
rg -n -C 3 'test-exception-approved|assessHygiene|exception|synchronize|reopened' .github MAINTAINERS.md 2>/dev/null || true

Repository: lidge-jun/opencodex

Length of output: 34223


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
text = Path(".github/workflows/pr-hygiene.yml").read_text()

trigger_line = "types: [opened, reopened, synchronize, labeled, unlabeled]"
condition = 'if (context.payload.action === "synchronize")'

assert trigger_line in text
assert condition in text

for action in ("synchronize", "reopened"):
    revokes = action == "synchronize"
    print(f"{action}: exception labels revoked = {revokes}")

print("reopened is configured as a trigger but is not included in the revocation predicate.")
PY

Repository: lidge-jun/opencodex

Length of output: 330


Revoke exception labels when a pull request is reopened

GitHub does not emit synchronize for commits pushed while a pull request is closed. Reopening emits only reopened. Because .github/workflows/pr-hygiene.yml:77-91 revokes labels only for synchronize, a close→push→reopen cycle preserves stale approvals, and assessHygiene accepts new violations. Include reopened in the revocation condition or compare the approved head SHA with pr.head.sha.

🧰 Tools
🪛 zizmor (1.28.0)

[error] 3-9: use of fundamentally insecure workflow trigger (dangerous-triggers): pull_request_target is almost always used insecurely

(dangerous-triggers)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-hygiene.yml around lines 3 - 5, Update the pr-hygiene
workflow’s label-revocation logic in assessHygiene to run for reopened pull
requests as well as synchronize events, ensuring close→push→reopen cycles revoke
stale exception labels before hygiene assessment.

Source: Path instructions

Comment on lines +54 to +67
async function ensureLabel(name) {
try {
await github.rest.issues.getLabel({ owner, repo, name });
} catch (error) {
if (error.status !== 404) throw error;
const [color, description] = labelDefinitions[name];
try {
await github.rest.issues.createLabel({ owner, repo, name, color, description });
} catch (createError) {
if (createError.status !== 422) throw createError;
}
}
}
for (const name of Object.keys(labelDefinitions)) await ensureLabel(name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

ensureLabel issues a GET (and a conditional POST) per label on every triggering event.

ensureLabel (Lines 54-66) is called once per entry in labelDefinitions (Line 67), so every opened, reopened, synchronize, labeled, and unlabeled event makes 5 getLabel calls even though these 5 labels almost never change after the first run. This is a minor, steady API-call tax rather than a functional problem, given GitHub's generous per-installation rate limits, but it's easy to cut: fetch the repo's label list once (github.rest.issues.listLabelsForRepo) and diff locally, only calling createLabel for the ones actually missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-hygiene.yml around lines 54 - 67, Replace the per-label
getLabel calls in ensureLabel and the Object.keys(labelDefinitions) loop with
one issues.listLabelsForRepo request to fetch existing labels, then diff the
returned names locally and call createLabel only for missing definitions.
Preserve the existing 422 handling for concurrent label creation and propagate
other errors.

Comment on lines +126 to +143
const explanations = {
missing_regression_test: "Behavior changed under `src/` or `gui/src/` without a test change. Add focused coverage or obtain `test-exception-approved`.",
generated_output: "Generated build output is committed. Remove it or obtain `generated-change-approved`.",
orphan_lockfile: "`bun.lock` changed without `package.json`. Revert accidental churn or obtain `dependency-change-approved`.",
new_suppression: "A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain `suppression-approved`.",
focused_or_skipped_test: "A focused or skipped test was added. Restore the complete suite or obtain `test-exception-approved`.",
empty_catch: "An empty catch block was added. Handle, report, or deliberately propagate the error.",
};
const lines = failures.map((failure) => {
const paths = failure.paths?.length
? ` Paths: ${failure.paths.map((p) => `\`${p}\``).join(", ")}.`
: "";
return `- **${failure.code}** — ${explanations[failure.code]}${paths}`;
});

await setBlocked(true);
await upsert([marker, "", "⚠️ **Deterministic hygiene checks failed.**", "", ...lines].join("\n"));
core.setFailed(`PR hygiene failed: ${failures.map((f) => f.code).join(", ")}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Untrusted PR filenames are embedded into the bot's own comment body without escaping backticks.

failure.paths (Line 135) comes directly from github.rest.pulls.listFiles filenames, which a PR author fully controls, including on Linux where backtick (`) is a legal filename character. Line 136 wraps each path in single backticks: `${p}`. A filename containing a backtick terminates the inline code span early, letting the remainder of the string render as arbitrary Markdown inside the github-actions[bot] comment (e.g., injecting a fake "approved" note, a phishing link, or content that looks like a maintainer instruction). Since this bot comment carries implicit trust (it drives the blockedLabel state maintainers rely on), this is a credible comment-spoofing vector introduced by a hostile PR.

Sanitize or strip backticks from p before interpolation:

🛡️ Proposed fix: neutralize backticks in embedded filenames
             const lines = failures.map((failure) => {
               const paths = failure.paths?.length
-                ? ` Paths: ${failure.paths.map((p) => `\`${p}\``).join(", ")}.`
+                ? ` Paths: ${failure.paths.map((p) => `\`${p.replace(/`/g, "'")}\``).join(", ")}.`
                 : "";
               return `- **${failure.code}** — ${explanations[failure.code]}${paths}`;
             });

As per path instructions for .github/**, workflow changes require explicit security review; this is exactly the class of finding (untrusted input flowing into a trust-bearing artifact) that review should catch.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const explanations = {
missing_regression_test: "Behavior changed under `src/` or `gui/src/` without a test change. Add focused coverage or obtain `test-exception-approved`.",
generated_output: "Generated build output is committed. Remove it or obtain `generated-change-approved`.",
orphan_lockfile: "`bun.lock` changed without `package.json`. Revert accidental churn or obtain `dependency-change-approved`.",
new_suppression: "A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain `suppression-approved`.",
focused_or_skipped_test: "A focused or skipped test was added. Restore the complete suite or obtain `test-exception-approved`.",
empty_catch: "An empty catch block was added. Handle, report, or deliberately propagate the error.",
};
const lines = failures.map((failure) => {
const paths = failure.paths?.length
? ` Paths: ${failure.paths.map((p) => `\`${p}\``).join(", ")}.`
: "";
return `- **${failure.code}** — ${explanations[failure.code]}${paths}`;
});
await setBlocked(true);
await upsert([marker, "", "⚠️ **Deterministic hygiene checks failed.**", "", ...lines].join("\n"));
core.setFailed(`PR hygiene failed: ${failures.map((f) => f.code).join(", ")}`);
const explanations = {
missing_regression_test: "Behavior changed under `src/` or `gui/src/` without a test change. Add focused coverage or obtain `test-exception-approved`.",
generated_output: "Generated build output is committed. Remove it or obtain `generated-change-approved`.",
orphan_lockfile: "`bun.lock` changed without `package.json`. Revert accidental churn or obtain `dependency-change-approved`.",
new_suppression: "A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain `suppression-approved`.",
focused_or_skipped_test: "A focused or skipped test was added. Restore the complete suite or obtain `test-exception-approved`.",
empty_catch: "An empty catch block was added. Handle, report, or deliberately propagate the error.",
};
const lines = failures.map((failure) => {
const paths = failure.paths?.length
? ` Paths: ${failure.paths.map((p) => `\`${p.replace(/`/g, "'")}\``).join(", ")}.`
: "";
return `- **${failure.code}** — ${explanations[failure.code]}${paths}`;
});
await setBlocked(true);
await upsert([marker, "", "⚠️ **Deterministic hygiene checks failed.**", "", ...lines].join("\n"));
core.setFailed(`PR hygiene failed: ${failures.map((f) => f.code).join(", ")}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-hygiene.yml around lines 126 - 143, Sanitize untrusted
filenames in the `failures.map` formatting before embedding them in the bot
comment: neutralize or remove backticks from each `p` value before the
inline-code interpolation, while preserving the existing path list and failure
message structure.

Source: Path instructions

Two real false positives, both reproduced before fixing.

Empty-catch scanning concatenated every hunk in a file's patch before looking
for `catch {}`. Hunks are disjoint windows onto the file, so a hunk ending at
`} catch (e) {` followed by one starting at `}` reads as an empty catch that
exists nowhere in the file. Scanning is per hunk now; a catch emptied within one
window is still caught.

The orphan-lockfile check tested `bun.lock` unconditionally while the
generated-output and regression-test checks beside it both exclude removals.
Deleting `bun.lock` adds no dependency, so it no longer fails. A lockfile that
MOVED still does — both sides of a rename count, which the old check also missed.

Reported by CodeRabbit on #918. Both driven red by restoring the old behavior.
@lidge-jun
lidge-jun merged commit fd1c5b4 into dev Aug 3, 2026
6 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d381c6bd3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

permissions:
contents: read
issues: write
pull-requests: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reduce pull-request permission to read

The workflow only calls pulls.get and pulls.listFiles; all label and comment mutations use the separately granted issues: write permission. Granting pull-requests: write therefore gives this pull_request_target job unnecessary authority to modify pull requests, contrary to the repository's least-privilege requirement. Set this permission to read.

AGENTS.md reference: .github/AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

Comment on lines +85 to +88
await github.rest.issues.removeLabel({
owner, repo, issue_number: pull_number, name,
});
labels.delete(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent label events from interrupting approval revocation

When a PR has multiple exception labels, each removeLabel emits an unlabeled event for this same workflow. Because the concurrency group uses cancel-in-progress: true, the first removal can cancel the synchronize run before the remaining labels are cleared; the replacement unlabeled run skips this revocation block and can assess the new head with a stale approval. Structure revocation so these self-generated events cannot interrupt it, or make every run validate approvals against the current head.

AGENTS.md reference: .github/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

line.startsWith("//") ||
line.startsWith("/*") ||
line.startsWith("*") ||
line.startsWith("#"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat TypeScript private fields as code

Treating every trimmed line beginning with # as a comment misclassifies TypeScript private fields and methods. For example, changing only #verifier = ""—a pattern already used under src/oauth/—makes both changed lines comment-only, so the behavior change passes without a regression test. Restrict hash comments to applicable file types or remove this case for src/ and gui/src/.

AGENTS.md reference: AGENTS.md:L199-L201

Useful? React with 👍 / 👎.

const BEHAVIOR_PREFIXES = ["src/", "gui/src/"];
const TEST_PREFIXES = ["tests/"];
const TEST_FILE_PATTERN = /(?:^|\/)(?:__tests__\/.*|[^/]+\.(?:test|spec)\.[^.]+)$/;
const SUPPRESSION_PATTERN = /(?:@ts-ignore|@ts-nocheck|eslint-disable|biome-ignore|prettier-ignore)/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include @ts-expect-error in suppression detection

The suppression pattern omits @ts-expect-error, even though it suppresses TypeScript diagnostics and is already used in this repository. A PR can therefore introduce new @ts-expect-error directives without producing new_suppression or requiring suppression-approved; include this directive in the pattern and its regression cases.

Useful? React with 👍 / 👎.

const TEST_PREFIXES = ["tests/"];
const TEST_FILE_PATTERN = /(?:^|\/)(?:__tests__\/.*|[^/]+\.(?:test|spec)\.[^.]+)$/;
const SUPPRESSION_PATTERN = /(?:@ts-ignore|@ts-nocheck|eslint-disable|biome-ignore|prettier-ignore)/;
const FOCUSED_TEST_PATTERN = /\b(?:describe|it|test)\.(?:only|skip)\s*\(/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect newly added todo tests

The pattern detects .only( and .skip( but not Bun's test.todo( or it.todo( forms. Consequently, a contributor can add a disabled placeholder test while the advertised focused_or_skipped_test check reports success. Add the todo form while retaining the intentional allowance for conditional platform skips such as skipIf.

Useful? React with 👍 / 👎.

".next/",
"node_modules/",
];
const BEHAVIOR_PREFIXES = ["src/", "gui/src/"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the published launcher in behavior paths

bin/ocx.mjs is the user-facing npm launcher and contains substantial update, service, and process-launch behavior, but it is outside both behavior prefixes. A PR can therefore change production launcher behavior without any test change or exception label. Include the runtime launcher path in behavior classification.

Useful? React with 👍 / 👎.

Comment on lines +127 to +129
const testsChanged = allPaths.some(
(path) => isTestPath(path) && !removedFilenames.has(path),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require a surviving runnable test file

testsChanged checks both sides of renames and accepts any extension matched by the broad test-name pattern. A source change can therefore pass by renaming tests/x.test.ts to a non-test location, or by adding a non-runnable file such as docs/note.test.md; neither adds regression coverage. Determine coverage from surviving current paths and restrict it to locations and extensions exercised by the repository's test commands.

AGENTS.md reference: AGENTS.md:L199-L201

Useful? React with 👍 / 👎.

Comment on lines +69 to +72
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number, per_page: 100,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail closed when the pull-request file list is truncated

GitHub's pull-request files endpoint returns at most 3,000 files, but pagination is used without comparing the result to the already-fetched pr.changed_files. For a larger PR, files beyond that cap are silently absent from every hygiene check, allowing a behavior or generated-output violation outside the returned subset to pass. Reject incomplete results or use another complete diff source when files.length !== pr.changed_files.

Useful? React with 👍 / 👎.

Comment on lines +46 to +48
function isGeneratedPath(path) {
return GENERATED_PREFIXES.some((prefix) => path.startsWith(prefix));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect generated directories below package roots

Most generated prefixes are checked only at the repository root because startsWith("dist/"), startsWith("coverage/"), and startsWith("node_modules/") do not match nested paths. Generated content such as docs-site/dist/, gui/coverage/, or docs-site/node_modules/ therefore bypasses generated_output without an approval label. Match these names as path segments or enumerate every package's generated directories.

Useful? React with 👍 / 👎.

Comment on lines +4 to +5
pull_request_target:
types: [opened, reopened, synchronize, labeled, unlabeled]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-run hygiene when the PR base changes

The file-list verdict depends on the PR base, but the trigger omits the edited activity. When a stacked child is retargeted after its parent lands, GitHub emits pull_request_target: edited; if the head SHA is unchanged, this workflow does not run, leaving the old green result and any exception labels in place even though the effective diff changed. Add edited and reassess the fresh PR state.

AGENTS.md reference: AGENTS.md:L158-L161

Useful? React with 👍 / 👎.

@lidge-jun
lidge-jun deleted the codex/pr-hygiene-gate branch August 3, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants