mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 17:02:19 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f9eb79e76 | ||
|
|
f1bee4c3a1 | ||
|
|
cf7a1616b7 | ||
|
|
e85ddc814d | ||
|
|
6da392ee9c | ||
|
|
4a91ac4b3a | ||
|
|
810d415e41 | ||
|
|
2e4045309f | ||
|
|
28fdec93da | ||
|
|
b6f9bc6c94 |
@@ -0,0 +1,110 @@
|
||||
name: PR Gate
|
||||
|
||||
# Strict PR usefulness gate (#3698): classifies every PR to master into
|
||||
# merge-lane / close-lane / needs-maintainer BEFORE any human review effort.
|
||||
# Verdict + reviewer checklist land in one sticky comment; exactly one
|
||||
# gate:* label is applied; close-lane exits 1 (red X = strong signal).
|
||||
#
|
||||
# SECURITY MODEL (pull_request_target on a 30k-star public repo):
|
||||
# - PR code is NEVER checked out or executed. Metadata + diff come from the
|
||||
# GitHub API only; the diff is capped at 120KB.
|
||||
# - The checkout below is the BASE repo (master) — rubric/script only.
|
||||
# NEVER add a `ref:` pointing at the PR head.
|
||||
# - Attacker-controlled values (title/body/diff) never touch the shell:
|
||||
# every ${{ }} is env-bound; run: scripts use plain env vars.
|
||||
# - Only the issues API is used (comments + labels), so issues:write is the
|
||||
# single write grant; the checkout drops its credentials.
|
||||
# - The mechanical CONTRIBUTING.md #3745 check (intent paragraph + screenshot)
|
||||
# runs BEFORE any API dependency, so a PR missing either still lands in
|
||||
# close-lane during an Anthropic outage — an outage is not a way through.
|
||||
# - If ANTHROPIC_API_KEY is missing or the API is unreachable on an otherwise
|
||||
# compliant PR, the script NEUTRAL-skips loudly (sticky comment + warning
|
||||
# annotation, exit 0) and CLEARS any stale gate:* label — never a silent
|
||||
# green, never a red X for a missing secret, never a stale verdict. A model
|
||||
# REFUSAL is not a skip: it routes to needs-maintainer so refusing is not a
|
||||
# way to dodge the gate.
|
||||
#
|
||||
# #3745 EXEMPTION (deliberate — mirrors policyExemption() in
|
||||
# scripts/pr-gate.mjs): the intent-paragraph + screenshot requirement filters
|
||||
# INCOMING OUTSIDE CONTRIBUTIONS. It is waived for repo owners / members /
|
||||
# collaborators, bot authors, and drafts.
|
||||
# Release automation cannot take a screenshot of itself, and without the
|
||||
# exemption every /ship release PR lands in close-lane
|
||||
# (measured: 40 of the last 40 merged PRs) — a check that is red
|
||||
# on every release gets switched off within a week, and then it filters
|
||||
# nothing. Exempt PRs still get the FULL usefulness verdict, the title rule and
|
||||
# every mechanical red flag; only the description requirement is skipped, and
|
||||
# the sticky comment says so on its own line.
|
||||
# author_association / draft / user.type are read from the pr.json fetched
|
||||
# below — GitHub-computed, not author-settable (except `draft`), and already
|
||||
# on disk, so nothing new is fetched and there is one source of truth.
|
||||
# `ready_for_review` is in the trigger list precisely because `draft` IS
|
||||
# author-settable: leaving draft re-runs the gate with the exemption gone, and
|
||||
# the exemption is folded into the spend-guard hash so the draft-era verdict
|
||||
# cannot be reused.
|
||||
# Pinned by test/pr-gate-workflow.test.ts.
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize, reopened, ready_for_review]
|
||||
branches: [master]
|
||||
|
||||
# issues:write is the ONLY write grant. Everything the script calls is the
|
||||
# issues API (comments, label create, label add/remove on the PR's issue), so
|
||||
# pull-requests:write would be a redundant second grant on the same objects.
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: pr-gate-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
# Base repo (master) only — provides scripts/pr-gate.mjs.
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
# Nothing here needs git auth after the clone; don't leave a token
|
||||
# in .git/config for the rest of the job.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Fetch PR metadata + diff (API only — PR code is never checked out)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/pr-gate"
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}" > "$RUNNER_TEMP/pr-gate/pr.json"
|
||||
# First 100 files is enough: red flags key off pr.json's changed_files
|
||||
# count, and >40 files already flags.
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" \
|
||||
> "$RUNNER_TEMP/pr-gate/files.json"
|
||||
# Diff via the .diff media type; GitHub can 406 on huge diffs —
|
||||
# degrade to a marker instead of failing the gate.
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}" \
|
||||
-H "Accept: application/vnd.github.diff" \
|
||||
> "$RUNNER_TEMP/pr-gate/pr.diff.full" \
|
||||
|| printf '[diff unavailable from the GitHub API — too large or unfetchable]\n' \
|
||||
> "$RUNNER_TEMP/pr-gate/pr.diff.full"
|
||||
MAX=122880 # 120KB cap
|
||||
if [ "$(wc -c < "$RUNNER_TEMP/pr-gate/pr.diff.full")" -gt "$MAX" ]; then
|
||||
head -c "$MAX" "$RUNNER_TEMP/pr-gate/pr.diff.full" > "$RUNNER_TEMP/pr-gate/pr.diff"
|
||||
printf '\n\n[TRUNCATED: diff capped at 120KB]\n' >> "$RUNNER_TEMP/pr-gate/pr.diff"
|
||||
else
|
||||
mv "$RUNNER_TEMP/pr-gate/pr.diff.full" "$RUNNER_TEMP/pr-gate/pr.diff"
|
||||
fi
|
||||
rm -f "$RUNNER_TEMP/pr-gate/pr.diff.full"
|
||||
|
||||
- name: Gate verdict (sticky comment + label; exit 1 only on close-lane)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: node scripts/pr-gate.mjs "$RUNNER_TEMP/pr-gate"
|
||||
@@ -0,0 +1,94 @@
|
||||
/** Type surface of scripts/pr-gate.mjs for test/pr-gate-workflow.test.ts (tsc-only). */
|
||||
export interface TitleCheck {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
export declare function checkTitle(title: string): TitleCheck;
|
||||
|
||||
export interface ChangedFile {
|
||||
filename: string;
|
||||
status: string;
|
||||
patch?: string;
|
||||
additions?: number;
|
||||
deletions?: number;
|
||||
}
|
||||
export interface RedFlag {
|
||||
id: string;
|
||||
detail: string;
|
||||
}
|
||||
export declare function detectRedFlags(input: {
|
||||
changedFiles: number;
|
||||
files: ChangedFile[];
|
||||
diff: string;
|
||||
}): RedFlag[];
|
||||
|
||||
export declare const RUBRIC: string;
|
||||
export declare const MAX_STRING: number;
|
||||
export declare const MAX_ITEMS: number;
|
||||
export declare const NET_SOURCE_LINE_LIMIT: number;
|
||||
export declare const DOWNGRADE_FLAG_IDS: string[];
|
||||
|
||||
/** CONTRIBUTING.md #3745: human-written intent paragraph + screenshot of gbrain in use. */
|
||||
export declare const CONTRIBUTING_URL: string;
|
||||
export declare const INTENT_MIN_WORDS: number;
|
||||
export declare const POLICY_FLAG_IDS: string[];
|
||||
export declare const POLICY_SCAN_MAX: number;
|
||||
export declare const POLICY_EXEMPT_ASSOCIATIONS: string[];
|
||||
export declare const AI_INTENT_DOWNGRADE: string;
|
||||
export declare function stripCodeFences(body: unknown): string;
|
||||
export declare const MODEL_BODY_MAX: number;
|
||||
export declare function modelBody(pr: { body?: string } | null | undefined): string;
|
||||
export declare function hasScreenshot(body: unknown): boolean;
|
||||
export declare function intentWordCount(body: unknown): number;
|
||||
export declare function hasIntentParagraph(body: unknown): boolean;
|
||||
export declare function detectPolicyMisses(body: unknown): RedFlag[];
|
||||
|
||||
export declare function sanitizeModelText(value: unknown, max?: number): string;
|
||||
export declare function sanitizeList(value: unknown, maxItems?: number, maxString?: number): string[];
|
||||
|
||||
export declare function applyMechanicalDowngrades(
|
||||
lane: string,
|
||||
flags: RedFlag[],
|
||||
intentAuthenticity?: string,
|
||||
): { lane: string; downgrades: string[] };
|
||||
|
||||
/** The pr.json fields the #3745 exemption reads (all GitHub-computed). */
|
||||
export interface PrIdentity {
|
||||
author_association?: string;
|
||||
draft?: boolean;
|
||||
user?: { type?: string; login?: string };
|
||||
}
|
||||
export declare function policyExemption(pr: PrIdentity | null | undefined): string | null;
|
||||
|
||||
export interface GhComment {
|
||||
id?: number;
|
||||
body?: unknown;
|
||||
user?: { type?: string; login?: string };
|
||||
}
|
||||
export declare function isOwnComment(comment: GhComment | null | undefined): boolean;
|
||||
|
||||
export declare function hashInputs(
|
||||
pr: PrIdentity & { title?: string; body?: string; head?: { sha?: string } },
|
||||
/** The assembled model payload (changed files + diff). runGate always passes it. */
|
||||
payload?: string,
|
||||
): string;
|
||||
export declare function parseState(body: unknown): { hash: string; lane?: string } | null;
|
||||
|
||||
export declare function renderComment(input: {
|
||||
lane?: string;
|
||||
verdict?: { confidence?: number; reasons?: unknown; reviewer_checklist?: unknown };
|
||||
titleCheck: TitleCheck;
|
||||
flags: RedFlag[];
|
||||
neutralReason?: string;
|
||||
downgrades?: string[];
|
||||
policyMisses?: RedFlag[];
|
||||
policyExempt?: string | null;
|
||||
labelsCleared?: boolean;
|
||||
state?: { hash: string; lane: string };
|
||||
}): string;
|
||||
|
||||
export declare function runGate(
|
||||
dir: string,
|
||||
env?: Record<string, string | undefined>,
|
||||
fetchImpl?: typeof fetch,
|
||||
): Promise<number>;
|
||||
+1190
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user