Skip to content
Merged
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
40 changes: 24 additions & 16 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8955,6 +8955,16 @@
"type": "number",
"nullable": true
},
"aiReviewLowConfidenceDisposition": {
"type": "string",
"nullable": true,
"enum": [
"one_shot",
"hold_for_review",
"advisory_only",
null
]
},
"aiReviewCombine": {
"type": "string",
"nullable": true,
Expand Down Expand Up @@ -9383,6 +9393,9 @@
"agentDryRun": {
"type": "boolean"
},
"agentGlobalFreezeOverride": {
"type": "boolean"
},
"contributorOpenPrCap": {
"type": "integer",
"nullable": true,
Expand Down Expand Up @@ -9524,6 +9537,14 @@
"moderationBannedLabel": {
"type": "string"
},
"skipAutomationBotAuthors": {
"type": "string",
"enum": [
"inherit",
"off",
"enabled"
]
},
"reviewEvasionProtection": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -9571,9 +9592,6 @@
"advisory"
]
},
"message": {
"type": "string"
},
"requireViewports": {
"type": "array",
"items": {
Expand All @@ -9586,6 +9604,9 @@
"type": "string"
}
},
"message": {
"type": "string"
},
"skillFileUrl": {
"type": "string"
}
Expand All @@ -9606,19 +9627,6 @@
"updatedAt": {
"type": "string",
"nullable": true
},
"agentGlobalFreezeOverride": {
"type": "boolean"
},
"aiReviewLowConfidenceDisposition": {
"type": "string",
"nullable": true,
"enum": [
"one_shot",
"hold_for_review",
"advisory_only",
null
]
}
},
"required": [
Expand Down
8 changes: 8 additions & 0 deletions migrations/0143_repository_skip_automation_bot_authors.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Waste elimination for known automation authors (github-actions[bot]/release-please, Renovate,
-- Dependabot -- settings/agent-actions.ts's PROTECTED_AUTOCLOSE_AUTHORS): skip AI review, gate evaluation,
-- and public-surface publish entirely for a PR genuinely triggered by one of these, not just suppress
-- output like review.auto_review.ignore_authors already does. 'inherit' (default) defers to the
-- GITTENSORY_SKIP_AUTOMATION_BOT_PRS global default (itself default-ON); 'off'/'enabled' fully override
-- the global default in either direction for this repo. Mirrors moderation_gate_mode's inherit/off/enabled
-- shape (0105).
ALTER TABLE repository_settings ADD COLUMN skip_automation_bot_authors TEXT NOT NULL DEFAULT 'inherit';
17 changes: 14 additions & 3 deletions scripts/check-docs-drift.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function extractRepositorySettingsFields(typesText) {
}

/** RepositorySettings fields deliberately excluded from the "every field must have SOME
* `.gittensory.yml.example` mention" check below, for two distinct reasons -- flagging either as "undocumented"
* `.gittensory.yml.example` mention" check below, for three distinct reasons -- flagging any as "undocumented"
* would be a false drift signal, not a real gap:
* - Not a maintainer-settable knob at all: `repoFullName` is the row's own identity key (set once at
* creation, the opposite of something a maintainer overrides via config); `createdAt`/`updatedAt` are
Expand All @@ -103,8 +103,19 @@ export function extractRepositorySettingsFields(typesText) {
* exclusion, with the same rationale, in `SETTINGS_OPERATOR_ONLY_FIELDS` in
* test/unit/focus-manifest.test.ts's `.gittensory.yml.example field-exhaustiveness` suite. (An #4617 audit
* pass first flagged this field as an undocumented gap without that context; cross-checking the existing
* exhaustiveness suite before "fixing" it here caught the false positive.) */
const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set(["repoFullName", "createdAt", "updatedAt", "agentGlobalFreezeOverride"]);
* exhaustiveness suite before "fixing" it here caught the false positive.)
* - `skipAutomationBotAuthors`: genuinely settable (global env default + per-repo `inherit`/`off`/`enabled`
* override, mirroring `moderationGateMode`'s shape), but DELIBERATELY not wired into the
* FocusManifest/`.gittensory.yml` parsing path -- DB-only for now, confirmed as an intentional scope choice
* for this feature rather than an oversight. It is correctly absent from `FocusManifestSettings` (so the
* separate `.gittensory.yml.example` field-exhaustiveness suite never expected a token for it either). */
const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set([
"repoFullName",
"createdAt",
"updatedAt",
"agentGlobalFreezeOverride",
"skipAutomationBotAuthors",
]);

/** RepositorySettings fields whose `.gittensory.yml.example` documentation exists under a DIFFERENT, shorter
* name than the field itself -- almost always because the yml groups several sibling fields under one named
Expand Down
9 changes: 9 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
moderationRules: undefined,
moderationWarningLabel: undefined,
moderationBannedLabel: undefined,
skipAutomationBotAuthors: "inherit",
reviewEvasionProtection: "close", // #4011: default-ON -- see normalizeReviewEvasionProtection's doc comment
reviewEvasionLabel: DEFAULT_REVIEW_EVASION_LABEL,
reviewEvasionComment: true,
Expand Down Expand Up @@ -659,6 +660,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
moderationRules: parseModerationRulesColumn(row.moderationRulesJson),
moderationWarningLabel: normalizeModerationLabel(row.moderationWarningLabel),
moderationBannedLabel: normalizeModerationLabel(row.moderationBannedLabel),
skipAutomationBotAuthors: normalizeSkipAutomationBotAuthors(row.skipAutomationBotAuthors),
reviewEvasionProtection: normalizeReviewEvasionProtection(row.reviewEvasionProtection),
reviewEvasionLabel: row.reviewEvasionLabel,
reviewEvasionComment: row.reviewEvasionComment,
Expand Down Expand Up @@ -777,6 +779,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
moderationRules: settings.moderationRules,
moderationWarningLabel: normalizeModerationLabel(settings.moderationWarningLabel),
moderationBannedLabel: normalizeModerationLabel(settings.moderationBannedLabel),
skipAutomationBotAuthors: normalizeSkipAutomationBotAuthors(settings.skipAutomationBotAuthors),
reviewEvasionProtection: normalizeReviewEvasionProtection(settings.reviewEvasionProtection),
reviewEvasionLabel: settings.reviewEvasionLabel ?? DEFAULT_REVIEW_EVASION_LABEL,
reviewEvasionComment: settings.reviewEvasionComment ?? true,
Expand Down Expand Up @@ -859,6 +862,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
moderationRulesJson: resolved.moderationRules === undefined ? null : jsonString(resolved.moderationRules),
moderationWarningLabel: resolved.moderationWarningLabel ?? null,
moderationBannedLabel: resolved.moderationBannedLabel ?? null,
skipAutomationBotAuthors: resolved.skipAutomationBotAuthors,
reviewEvasionProtection: resolved.reviewEvasionProtection,
reviewEvasionLabel: resolved.reviewEvasionLabel,
reviewEvasionComment: resolved.reviewEvasionComment,
Expand Down Expand Up @@ -949,6 +953,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
moderationRulesJson: resolved.moderationRules === undefined ? null : jsonString(resolved.moderationRules),
moderationWarningLabel: resolved.moderationWarningLabel ?? null,
moderationBannedLabel: resolved.moderationBannedLabel ?? null,
skipAutomationBotAuthors: resolved.skipAutomationBotAuthors,
reviewEvasionProtection: resolved.reviewEvasionProtection,
reviewEvasionLabel: resolved.reviewEvasionLabel,
reviewEvasionComment: resolved.reviewEvasionComment,
Expand Down Expand Up @@ -7497,6 +7502,10 @@ function normalizeModerationGateMode(value: string | null | undefined): "inherit
return value === "off" || value === "enabled" ? value : "inherit";
}

function normalizeSkipAutomationBotAuthors(value: string | null | undefined): "inherit" | "off" | "enabled" {
return value === "off" || value === "enabled" ? value : "inherit";
}

// NULL means "inherit the global rule set" (undefined), distinct from a normalized-but-empty list -- a repo
// that explicitly configured an empty moderationRules override (opting every rule out) must stay empty, not
// be coerced back to "inherit". Mirrors parseContributorBlacklist/parseAutoCloseExemptLogins's JSON-parse
Expand Down
4 changes: 4 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export const repositorySettings = sqliteTable("repository_settings", {
// Contributor skill-file link appended to the auto-generated matrix/presence rejection message (#4540
// follow-up). Nullable, same "no override configured" shape as screenshotTableGateMessage above.
screenshotTableGateSkillFileUrl: text("screenshot_table_gate_skill_file_url"),
// Waste elimination for known automation authors (settings/automation-bot-skip.ts). 'inherit' (default)
// defers to the GITTENSORY_SKIP_AUTOMATION_BOT_PRS global default; 'off'/'enabled' force this repo
// regardless of it -- mirrors moderationGateMode's shape above.
skipAutomationBotAuthors: text("skip_automation_bot_authors").notNull().default("inherit"),
createdAt: text("created_at").notNull().$defaultFn(() => nowIso()),
updatedAt: text("updated_at").notNull().$defaultFn(() => nowIso()),
});
Expand Down
6 changes: 6 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,12 @@ declare global {
* flagged (see the same-author guard in buildCollisionReport). Default OFF — unset/false leaves every
* PullRequestRecord's changedFiles unset, byte-identical to today. See src/signals/engine.ts prItem. */
GITTENSORY_OPEN_PR_FILE_COLLISION?: string;
/** Waste elimination for known automation authors (settings/automation-bot-skip.ts): skip AI review, gate
* evaluation, and public-surface publish entirely for a PR/event genuinely triggered by release-please's
* github-actions[bot], Renovate, or Dependabot. Default-ON, unlike most flags above — see that module's
* own doc comment for why. A repo can override via its own repository_settings.skip_automation_bot_authors
* column ("off"/"enabled"), independent of this global default. */
GITTENSORY_SKIP_AUTOMATION_BOT_PRS?: string;
/** D1 size/row-count observability probe (#3810): the Cloudflare account id that owns the D1 database to
* monitor. Presence of this AND the two vars below IS the enablement switch (see isD1SizeProbeEnabled,
* src/selfhost/d1-size-probe.ts) -- unset/blank ⇒ the probe never runs, byte-identical to today. Most
Expand Down
1 change: 1 addition & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ export const RepositorySettingsSchema = z
moderationRules: z.array(z.enum(["contributor_cap", "blacklist", "review_nag", "review_evasion"])).optional(),
moderationWarningLabel: z.string().optional(),
moderationBannedLabel: z.string().optional(),
skipAutomationBotAuthors: z.enum(["inherit", "off", "enabled"]).optional(),
reviewEvasionProtection: z.enum(["off", "close"]).optional(),
reviewEvasionLabel: z.string().nullable().optional(),
reviewEvasionComment: z.boolean().optional(),
Expand Down
58 changes: 58 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,12 @@ import {
type PlannedAgentAction,
} from "../settings/agent-actions";
import { isAutoCloseExempt } from "../settings/auto-close-exempt";
import {
isSkipAutomationBotPullRequestsEnabledGlobally,
isTrustedAutomationBotAuthor,
isTrustedAutomationBotWebhookActor,
resolveSkipAutomationBotPullRequests,
} from "../settings/automation-bot-skip";
import { resolveGlobalContributorOpenItemCap, resolveGlobalContributorOpenItemCapForMiner } from "../settings/global-contributor-cap";
import { detectMigrationCollisions, extractMigrationNumber, KNOWN_MIGRATION_DUPLICATES } from "../db/migration-collisions";
import { listMigrationFilenamesAtRef } from "../github/migration-tree";
Expand Down Expand Up @@ -3669,6 +3675,17 @@ async function reReviewStoredPullRequest(
]);
let pr = await getPullRequest(env, repoFullName, prNumber);
if (!pr || pr.state !== "open") return;
// Waste elimination for known automation authors (settings/automation-bot-skip.ts): every re-entry path
// that can reach a PR without a fresh webhook (a scheduled sweep, CI-completion, or linked-issue-change
// re-review -- all funnel through this function via regatePullRequest) funnels through here, so this one
// check closes all of them. Uses the STORED author (isTrustedAutomationBotAuthor), not a live webhook
// sender -- see that function's own doc comment for why that's still safe: authorLogin is immutable,
// GitHub-attested metadata already verified against the actor at the original `opened` webhook.
if (
resolveSkipAutomationBotPullRequests(isSkipAutomationBotPullRequestsEnabledGlobally(env), settings.skipAutomationBotAuthors) &&
isTrustedAutomationBotAuthor(pr.authorLogin)
)
return;
const autoreviewPaused = await hasAutoreviewPausedMarker(env, repoFullName, prNumber);
const liveFacts = createLiveGithubFacts();
// #sweep-resync: RESYNC the stored PR to its LIVE head before reviewing. The self-host relay can drop the
Expand Down Expand Up @@ -6321,6 +6338,47 @@ async function handlePullRequestWebhookEvent(
// Resolve settings first so the self-authored + open-reference live-fetch fallbacks only fire when their
// respective gates are in block mode.
const settings = await resolveRepositorySettings(env, repoFullName);
// Waste elimination for known automation authors (settings/automation-bot-skip.ts): a PR/event genuinely
// triggered by release-please's github-actions[bot], Renovate, or Dependabot never needs AI review, gate
// evaluation, or a public-surface publish. Checked here (not earlier) because it needs `settings` for the
// per-repo override, but BEFORE the expensive Promise.all/refreshPullRequestDetails/AI/gate work below --
// isTrustedAutomationBotWebhookActor is the security-critical check (see its own doc comment): it verifies
// the ACTOR WHO TRIGGERED THIS EVENT, not just the PR's stored author, so a human pushing to an existing
// bot PR's branch still gets full review of their own commits.
if (
resolveSkipAutomationBotPullRequests(isSkipAutomationBotPullRequestsEnabledGlobally(env), settings.skipAutomationBotAuthors) &&
isTrustedAutomationBotWebhookActor(payload.sender, pr.authorLogin)
) {
await recordAuditEvent(env, {
eventType: "github_app.automation_bot_pr_skipped",
actor: payload.sender?.login ?? pr.authorLogin,
targetKey: `${repoFullName}#${pr.number}`,
outcome: "completed",
detail: "skipped: known automation-bot author (release-please/Renovate/Dependabot)",
metadata: { deliveryId, repoFullName, eventName, action: payload.action ?? null },
}).catch((error) => {
/* v8 ignore next -- best-effort: audit recording never blocks (or un-skips) the webhook. */
console.warn(
JSON.stringify({
level: "warn",
event: "automation_bot_pr_skip_audit_failed",
deliveryId,
repository: repoFullName,
error: errorMessage(error),
}),
);
});
await recordWebhookEvent(env, {
deliveryId,
eventName,
action: payload.action,
installationId: payload.installation?.id,
repositoryFullName: payload.repository?.full_name,
payloadHash: "processed",
status: "processed",
});
return true;
}
const [repo, cachedOtherOpenPullRequests, { linkedIssueAuthorLogins, confirmedNoOpenLinkedIssue }] =
await Promise.all([
getRepository(env, repoFullName),
Expand Down
Loading
Loading