Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Default reviewers
* @lidge-jun @Ingwannu @Wibias

# High-impact runtime behavior
/src/adapters/ @lidge-jun @Ingwannu @Wibias
/src/providers/ @lidge-jun @Ingwannu @Wibias
/src/codex/ @lidge-jun @Ingwannu @Wibias
/src/server/ @lidge-jun @Ingwannu @Wibias

# Repository automation and release security
/.github/ @lidge-jun @Ingwannu
/scripts/release.ts @lidge-jun @Ingwannu
Expand Down
67 changes: 67 additions & 0 deletions .github/CONTRIBUTION_FIREWALL_ROLLOUT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Contribution firewall rollout

The workflows in the five-PR stack are inert for external pull requests until their trusted scripts and workflow definitions are on the repository default branch. Do not configure required checks before the synthetic-fork validation below.

## 1. Promote trusted automation

Promote the merged `dev` versions of these files to the default branch:

- `.github/workflows/enforce-pr-target.yml`
- `.github/workflows/pr-admission.yml`
- `.github/workflows/pr-readiness.yml`
- `.github/workflows/pr-trust-lane.yml`
- `.github/workflows/pr-hygiene.yml`
- `.github/workflows/pr-review-lifecycle.yml`
- `.github/workflows/stale-author-prs.yml`
- their corresponding `.github/scripts/*.cjs` files
- `.coderabbit.yaml`

## 2. Synthetic fork test

Open a fork PR against `dev` and prove each transition:

1. Missing approved issue and unchecked attestations fail admission and produce `awaiting-author`.
2. Correcting intake moves to `intake: validating` while checks run.
3. A failing CI or CodeRabbit check returns to `awaiting-author` and keeps the PR draft.
4. All checks passing produces `awaiting-maintainer` and restores ready-for-review only when automation owned the draft.
5. A first-time contributor is blocked by a second active implementation PR, an unapproved change over 500 lines, and a restricted security/release surface without sponsorship.
6. Hygiene fixtures prove missing tests, suppressions, focused tests, empty catches, generated output, and lockfile churn fail.
7. Two `CHANGES_REQUESTED` reviews on distinct head SHAs produce `review: limit-reached`; duplicate reviews on one SHA do not increment.
8. `awaiting-author` stales after three inactive days and closes after two more; `awaiting-maintainer` never stales.

## 3. Configure the `dev` ruleset (owner/admin)

The connected `Wibias` account has write access but not repository admin access, so the project owner or another administrator must perform this step.

- Require pull requests before merging.
- Require at least one approval and prevent author self-approval.
- Require CODEOWNERS approval.
- Dismiss stale approvals when new commits are pushed.
- Require approval of the most recent reviewable push.
- Require all review conversations to be resolved.
- Require these checks after their exact names are confirmed by the synthetic test:
- `Enforce PR target branch / enforce-target`
- `PR admission / admission`
- `PR readiness / reconcile`
- `PR trust lane / trust-lane`
- `PR hygiene / hygiene`
- the cross-platform CI jobs required by current release policy

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 Make required CI report for path-filtered pull requests

The proposed ruleset globally requires the cross-platform CI jobs, while .github/workflows/ci.yml runs on pull requests only when one of its paths entries changes. A documentation-only or governance-only PR therefore never creates those required checks and remains blocked indefinitely; remove the workflow-level path filter or add an always-running required aggregate check that succeeds when the expensive matrix is intentionally skipped.

Useful? React with 👍 / 👎.

- CodeRabbit's blocking review check
- Restrict bypass permissions to emergency owner/maintainer recovery only.

## 4. Enable merge queue

Enable the merge queue for `dev` after required checks are stable. Require queued commits to rerun the same checks against the current integration state. Do not enable auto-merge as a substitute for approvals or unresolved-thread checks.
Comment on lines +52 to +54

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 Add merge-group support before enabling the queue

The rollout instructs the administrator to enable a merge queue and rerun the same required checks, but none of the listed required workflows (enforce-pr-target, admission, readiness, trust lane, hygiene, or cross-platform CI) declares a merge_group trigger. Once the queue is enabled, those checks therefore do not report for the synthetic merge-group commit and queued PRs cannot merge; add merge-group-capable checks and adapt PR-specific jobs before performing this step.

Useful? React with 👍 / 👎.


## 5. Measure before tightening

For two weeks, record:

- admission failure rate;
- abandonment and reopen rate;
- first-pass CI success;
- substantial review rounds per merged PR;
- maintainer review time;
- closures by standardized reason.

Change thresholds only from this evidence. Commit count and guessed AI origin are not quality metrics.
63 changes: 63 additions & 0 deletions .github/scripts/pr-review-lifecycle.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use strict";

const MAX_SUBSTANTIAL_REVIEW_ROUNDS = 2;
const CLOSURE_LABELS = [
"close: no-approved-issue",
"close: not-review-ready",
"close: abandoned",
"close: excessive-review-churn",
"close: scope-too-large",
"close: wrong-direction",
"close: insufficient-tests",
];

function normalizeState(state) {
return {
version: 1,
rounds: Number.isInteger(state?.rounds) && state.rounds >= 0 ? state.rounds : 0,
lastCountedHeadSha:
typeof state?.lastCountedHeadSha === "string" ? state.lastCountedHeadSha : null,
};
}

function isSubstantialReview(body) {
if (typeof body !== "string") return false;
const text = body.replace(/<!--[^]*?-->/g, "").trim();
return text.length >= 40;
}

function applyReviewEvent({
state,
reviewState,
reviewBody,
reviewerHasPushPermission,
headSha,
}) {
const current = normalizeState(state);
const result = {
...current,
counted: false,
limitReached: current.rounds >= MAX_SUBSTANTIAL_REVIEW_ROUNDS,
};

if (String(reviewState || "").toLowerCase() !== "changes_requested") return result;
if (!reviewerHasPushPermission || !isSubstantialReview(reviewBody)) return result;
if (!headSha || headSha === current.lastCountedHeadSha) return result;

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 Track every counted SHA rather than only the latest one

When an author force-pushes a previously reviewed head again—for example, the sequence A → B → A—the final A differs from lastCountedHeadSha and is counted a second time. This violates the documented once-per-distinct-SHA rule and can reach the limit with only one unique revision being counted twice; persist the set of counted head SHAs instead of only the last value.

Useful? React with 👍 / 👎.


const rounds = current.rounds + 1;
return {
version: 1,
rounds,
lastCountedHeadSha: headSha,
counted: true,
limitReached: rounds >= MAX_SUBSTANTIAL_REVIEW_ROUNDS,
};
}

module.exports = {
CLOSURE_LABELS,
MAX_SUBSTANTIAL_REVIEW_ROUNDS,
applyReviewEvent,
isSubstantialReview,
normalizeState,
};
96 changes: 96 additions & 0 deletions .github/scripts/pr-review-lifecycle.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use strict";

const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const {
CLOSURE_LABELS,
MAX_SUBSTANTIAL_REVIEW_ROUNDS,
applyReviewEvent,
isSubstantialReview,
} = require("./pr-review-lifecycle.cjs");

const body = "The implementation still violates the routing boundary and needs a focused regression test.";

describe("review round accounting", () => {
it("counts a substantial maintainer change request", () => {
const result = applyReviewEvent({
reviewState: "changes_requested",
reviewBody: body,
reviewerHasPushPermission: true,
headSha: "aaa",
});
assert.equal(result.rounds, 1);
assert.equal(result.counted, true);
assert.equal(result.limitReached, false);
});

it("counts at most once per reviewed head SHA", () => {
const result = applyReviewEvent({
state: { rounds: 1, lastCountedHeadSha: "aaa" },
reviewState: "changes_requested",
reviewBody: body,
reviewerHasPushPermission: true,
headSha: "aaa",
});
assert.equal(result.rounds, 1);
assert.equal(result.counted, false);
});

it("does not count non-maintainer or thin reviews", () => {
assert.equal(applyReviewEvent({
reviewState: "changes_requested",
reviewBody: body,
reviewerHasPushPermission: false,
headSha: "aaa",
}).rounds, 0);
assert.equal(applyReviewEvent({
reviewState: "changes_requested",
reviewBody: "fix this",
reviewerHasPushPermission: true,
headSha: "aaa",
}).rounds, 0);
});

it("flags the limit after two distinct reviewed revisions", () => {
const result = applyReviewEvent({
state: { rounds: 1, lastCountedHeadSha: "aaa" },
reviewState: "changes_requested",
reviewBody: body,
reviewerHasPushPermission: true,
headSha: "bbb",
});
assert.equal(MAX_SUBSTANTIAL_REVIEW_ROUNDS, 2);
assert.equal(result.rounds, 2);
assert.equal(result.limitReached, true);
});

it("ignores approvals and comments", () => {
for (const reviewState of ["approved", "commented", "dismissed"]) {
assert.equal(applyReviewEvent({
reviewState,
reviewBody: body,
reviewerHasPushPermission: true,
headSha: "aaa",
}).rounds, 0);
}
});
});

describe("policy constants", () => {
it("recognizes substantive review text", () => {
assert.equal(isSubstantialReview(body), true);
assert.equal(isSubstantialReview("too short"), false);
});

it("exports the complete closure taxonomy", () => {
assert.deepEqual(CLOSURE_LABELS, [
"close: no-approved-issue",
"close: not-review-ready",
"close: abandoned",
"close: excessive-review-churn",
"close: scope-too-large",
"close: wrong-direction",
"close: insufficient-tests",
]);
});
});
56 changes: 26 additions & 30 deletions .github/workflows/issue-quality-tests.yml
Original file line number Diff line number Diff line change
@@ -1,48 +1,42 @@
name: Issue quality tests
name: Issue and PR policy tests

on:
pull_request:
paths:
- ".github/ISSUE_TEMPLATE/**"
- ".github/scripts/issue-quality.cjs"
- ".github/scripts/issue-quality.test.cjs"
- ".github/scripts/pr-quality.cjs"
- ".github/scripts/pr-quality.test.cjs"
- ".github/scripts/pr-labeler.cjs"
- ".github/scripts/pr-labeler.test.cjs"
- ".github/scripts/enforce-pr-target.test.cjs"
- ".github/scripts/issue-translation.cjs"
- ".github/scripts/issue-translation.test.cjs"
- ".github/scripts/issue-triage.cjs"
- ".github/scripts/issue-triage.test.cjs"
- ".github/scripts/parse-issue-translation-response.cjs"
- ".github/scripts/parse-issue-translation-response.test.cjs"
- ".github/PULL_REQUEST_TEMPLATE.md"
- ".github/scripts/*.cjs"
- ".github/workflows/enforce-issue-quality.yml"
- ".github/workflows/enforce-pr-target.yml"
- ".github/workflows/pr-admission.yml"
- ".github/workflows/pr-readiness.yml"
- ".github/workflows/pr-trust-lane.yml"
- ".github/workflows/pr-hygiene.yml"
- ".github/workflows/pr-review-lifecycle.yml"
- ".github/workflows/pr-labeler.yml"
- ".github/workflows/issue-triage.yml"
- ".github/workflows/issue-quality-tests.yml"
- ".coderabbit.yaml"
- "CONTRIBUTING.md"
- "MAINTAINERS.md"
push:
paths:
- ".github/ISSUE_TEMPLATE/**"
- ".github/scripts/issue-quality.cjs"
- ".github/scripts/issue-quality.test.cjs"
- ".github/scripts/pr-quality.cjs"
- ".github/scripts/pr-quality.test.cjs"
- ".github/scripts/pr-labeler.cjs"
- ".github/scripts/pr-labeler.test.cjs"
- ".github/scripts/enforce-pr-target.test.cjs"
- ".github/scripts/issue-translation.cjs"
- ".github/scripts/issue-translation.test.cjs"
- ".github/scripts/issue-triage.cjs"
- ".github/scripts/issue-triage.test.cjs"
- ".github/scripts/parse-issue-translation-response.cjs"
- ".github/scripts/parse-issue-translation-response.test.cjs"
- ".github/PULL_REQUEST_TEMPLATE.md"
- ".github/scripts/*.cjs"
- ".github/workflows/enforce-issue-quality.yml"
- ".github/workflows/enforce-pr-target.yml"
- ".github/workflows/pr-admission.yml"
- ".github/workflows/pr-readiness.yml"
- ".github/workflows/pr-trust-lane.yml"
- ".github/workflows/pr-hygiene.yml"
- ".github/workflows/pr-review-lifecycle.yml"
- ".github/workflows/pr-labeler.yml"
- ".github/workflows/issue-triage.yml"
- ".github/workflows/issue-quality-tests.yml"
- ".coderabbit.yaml"
- "CONTRIBUTING.md"
- "MAINTAINERS.md"

permissions:
contents: read
Expand All @@ -61,6 +55,11 @@ jobs:
run: |
node --test .github/scripts/issue-quality.test.cjs
node --test .github/scripts/pr-quality.test.cjs
node --test .github/scripts/pr-admission.test.cjs
node --test .github/scripts/pr-readiness.test.cjs
node --test .github/scripts/pr-trust-lane.test.cjs
node --test .github/scripts/pr-hygiene.test.cjs
node --test .github/scripts/pr-review-lifecycle.test.cjs
node --test .github/scripts/pr-labeler.test.cjs
node --test .github/scripts/enforce-pr-target.test.cjs
node --test .github/scripts/issue-translation.test.cjs
Expand All @@ -78,16 +77,13 @@ jobs:
let ok = true;
for (const file of files) {
const raw = fs.readFileSync(path.join(dir, file), 'utf8');
// Minimal YAML validation: check for required keys and valid types.
if (!raw.includes('name:')) { console.error(file + ': missing name'); ok = false; }
if (!raw.includes('description:')) { console.error(file + ': missing description'); ok = false; }
if (!raw.includes('body:')) { console.error(file + ': missing body'); ok = false; }
// Check element types.
const types = [...raw.matchAll(/type:\s*(\w+)/g)].map(m => m[1]);
for (const t of types) {
if (!VALID_TYPES.has(t)) { console.error(file + ': unsupported element type: ' + t); ok = false; }
}
// Check unique IDs.
const ids = [...raw.matchAll(/id:\s*([\w-]+)/g)].map(m => m[1]);
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
if (dupes.length) { console.error(file + ': duplicate IDs: ' + dupes.join(', ')); ok = false; }
Expand Down
Loading
Loading