From e9a4b1dce25747daf862a44161707064264765e3 Mon Sep 17 00:00:00 2001 From: Caleb Aston Date: Tue, 28 Jul 2026 14:32:53 -0600 Subject: [PATCH 1/4] Add postChecks toggle to disable GitHub Check-run posting Check-run creation is entirely write-only (nothing in Warden reads it back, and it doesn't gate dedup or the findings-file build), so it's safe to let it be switched off independently of PR review comments. Defaults to true to preserve existing behavior; a defaults.postChecks in warden.toml can override the post-checks action input. Co-Authored-By: Claude Sonnet 5 --- action.yml | 5 + packages/warden/src/action/inputs.test.ts | 23 +++ packages/warden/src/action/inputs.ts | 4 + packages/warden/src/action/runner.test.ts | 1 + .../warden/src/action/workflow/base.test.ts | 1 + .../src/action/workflow/pr-workflow.test.ts | 103 +++++++++++++ .../warden/src/action/workflow/pr-workflow.ts | 145 ++++++++++-------- .../src/action/workflow/schedule.test.ts | 1 + packages/warden/src/config/loader.test.ts | 41 +++++ packages/warden/src/config/schema.ts | 2 + 10 files changed, 265 insertions(+), 61 deletions(-) diff --git a/action.yml b/action.yml index 410a5d3c3..abaee4c06 100644 --- a/action.yml +++ b/action.yml @@ -51,6 +51,10 @@ inputs: description: 'Whether to fail the check run when findings exceed fail-on threshold' required: false default: 'false' + post-checks: + description: 'Whether to create/update GitHub Check runs (core "warden" check and per-skill "warden: " checks). Set to false to disable all check-run writes while still posting PR review comments.' + required: false + default: 'true' parallel: description: 'Maximum number of concurrent trigger executions' required: false @@ -89,5 +93,6 @@ runs: INPUT_MAX_FINDINGS: ${{ inputs.max-findings }} INPUT_REQUEST_CHANGES: ${{ inputs.request-changes }} INPUT_FAIL_CHECK: ${{ inputs.fail-check }} + INPUT_POST_CHECKS: ${{ inputs.post-checks }} INPUT_PARALLEL: ${{ inputs.parallel }} run: node ${{ github.action_path }}/dist/action/index.js diff --git a/packages/warden/src/action/inputs.test.ts b/packages/warden/src/action/inputs.test.ts index 0d29906e4..b9911557d 100644 --- a/packages/warden/src/action/inputs.test.ts +++ b/packages/warden/src/action/inputs.test.ts @@ -82,6 +82,23 @@ describe('parseActionInputs', () => { const inputs = parseActionInputs(); expect(inputs.failCheck).toBeUndefined(); }); + + it('parses post-checks as true', () => { + process.env['INPUT_POST_CHECKS'] = 'true'; + const inputs = parseActionInputs(); + expect(inputs.postChecks).toBe(true); + }); + + it('parses post-checks as false', () => { + process.env['INPUT_POST_CHECKS'] = 'false'; + const inputs = parseActionInputs(); + expect(inputs.postChecks).toBe(false); + }); + + it('defaults postChecks to true when not set', () => { + const inputs = parseActionInputs(); + expect(inputs.postChecks).toBe(true); + }); }); describe('numeric input handling', () => { @@ -172,6 +189,7 @@ describe('setupAuthEnv', () => { mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, }); expect(process.env['ANTHROPIC_API_KEY']).toBe('sk-ant-api-key'); @@ -187,6 +205,7 @@ describe('setupAuthEnv', () => { mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, }); expect(process.env['CLAUDE_CODE_OAUTH_TOKEN']).toBe('sk-ant-oat-oauth-token'); @@ -205,6 +224,7 @@ describe('setupAuthEnv', () => { mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, }); @@ -225,6 +245,7 @@ describe('setupAuthEnv', () => { mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, }); @@ -244,6 +265,7 @@ describe('validateInputs', () => { baseSkillRoot: '.warden-org', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, })).toThrow('base-skill-root requires base-config-path'); }); @@ -256,6 +278,7 @@ describe('validateInputs', () => { mode: 'report', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, })).toThrow('findings-file is required when mode is report'); }); diff --git a/packages/warden/src/action/inputs.ts b/packages/warden/src/action/inputs.ts index da7b43a93..b5a300e79 100644 --- a/packages/warden/src/action/inputs.ts +++ b/packages/warden/src/action/inputs.ts @@ -37,6 +37,8 @@ export interface ActionInputs { requestChanges?: boolean; /** Whether to fail the check run when findings exceed failOn */ failCheck?: boolean; + /** Whether to create/update GitHub Check runs. Default true; explicit false disables the core check and all per-skill checks. */ + postChecks: boolean; /** Max concurrent trigger executions */ parallel: number; } @@ -111,6 +113,7 @@ export function parseActionInputs(): ActionInputs { const requestChanges = parseBooleanInput(getInput('request-changes')); const failCheck = parseBooleanInput(getInput('fail-check')); + const postChecks = parseBooleanInput(getInput('post-checks')) ?? true; return { anthropicApiKey, @@ -126,6 +129,7 @@ export function parseActionInputs(): ActionInputs { maxFindings: Number.isNaN(maxFindingsParsed) ? 50 : maxFindingsParsed, requestChanges, failCheck, + postChecks, parallel: Number.isNaN(parallelParsed) ? DEFAULT_CONCURRENCY : parallelParsed, }; } diff --git a/packages/warden/src/action/runner.test.ts b/packages/warden/src/action/runner.test.ts index 8c68fff5b..bedb9e37f 100644 --- a/packages/warden/src/action/runner.test.ts +++ b/packages/warden/src/action/runner.test.ts @@ -48,6 +48,7 @@ const baseInputs: ActionInputs = { mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, }; diff --git a/packages/warden/src/action/workflow/base.test.ts b/packages/warden/src/action/workflow/base.test.ts index 37c2a61e6..f5a32fe93 100644 --- a/packages/warden/src/action/workflow/base.test.ts +++ b/packages/warden/src/action/workflow/base.test.ts @@ -174,6 +174,7 @@ function createInputs(overrides: Partial = {}): ActionInputs { mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 4, ...overrides, }; diff --git a/packages/warden/src/action/workflow/pr-workflow.test.ts b/packages/warden/src/action/workflow/pr-workflow.test.ts index ea198ea62..6d209aa94 100644 --- a/packages/warden/src/action/workflow/pr-workflow.test.ts +++ b/packages/warden/src/action/workflow/pr-workflow.test.ts @@ -227,6 +227,7 @@ function createDefaultInputs(overrides: Partial = {}): ActionInput mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 2, ...overrides, }; @@ -1905,6 +1906,108 @@ describe('runPRWorkflow', () => { }) ); }); + + it('does not create or update any checks when postChecks is false, but still posts the review', async () => { + const finding = createFinding(); + const report = createSkillReport({ findings: [finding] }); + mockRunSkillTask.mockResolvedValue({ name: 'test-trigger', report }); + + await runPRWorkflow( + mockOctokit, + createDefaultInputs({ postChecks: false }), + 'pull_request', + EVENT_PAYLOAD_PATH, + FIXTURES_DIR + ); + + expect(mockOctokit.checks.create).not.toHaveBeenCalled(); + expect(mockOctokit.checks.update).not.toHaveBeenCalled(); + expect(mockOctokit.pulls.createReview).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'test-owner', + repo: 'test-repo', + pull_number: 123, + commit_id: PR_HEAD_SHA, + }) + ); + }); + + it('warden.toml defaults.postChecks overrides the post-checks action input', async () => { + const finding = createFinding(); + const report = createSkillReport({ findings: [finding] }); + mockRunSkillTask.mockResolvedValue({ name: 'test-trigger', report }); + + const tempDir = mkdtempSync(join(tmpdir(), 'warden-post-checks-config-')); + writeFileSync( + join(tempDir, 'warden.toml'), + [ + 'version = 1', + '', + '[defaults]', + 'postChecks = false', + '', + '[[skills]]', + 'name = "test-skill"', + '', + '[[skills.triggers]]', + 'type = "pull_request"', + 'actions = ["opened", "synchronize"]', + '', + ].join('\n') + ); + + try { + await runPRWorkflow( + mockOctokit, + createDefaultInputs({ postChecks: true }), + 'pull_request', + EVENT_PAYLOAD_PATH, + tempDir + ); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + + expect(mockOctokit.checks.create).not.toHaveBeenCalled(); + expect(mockOctokit.checks.update).not.toHaveBeenCalled(); + expect(mockOctokit.pulls.createReview).toHaveBeenCalled(); + }); + + it('report mode does not create or update any checks when postChecks is false, but still posts the review', async () => { + const finding = createFinding(); + const report = createSkillReport({ findings: [finding] }); + const findingsFile = writeFindingsArtifact([report], [ + { + triggerName: 'test-skill', + skillName: 'test-skill', + report, + }, + ]); + + try { + await runPRWorkflow( + mockOctokit, + createDefaultInputs({ mode: 'report', findingsFile, postChecks: false }), + 'pull_request', + EVENT_PAYLOAD_PATH, + FIXTURES_DIR + ); + } finally { + rmSync(dirname(findingsFile), { recursive: true, force: true }); + } + + expect(mockRunSkillTask).not.toHaveBeenCalled(); + expect(mockOctokit.checks.create).not.toHaveBeenCalled(); + expect(mockOctokit.checks.update).not.toHaveBeenCalled(); + expect(mockOctokit.pulls.createReview).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'test-owner', + repo: 'test-repo', + pull_number: 123, + commit_id: PR_HEAD_SHA, + }) + ); + }); }); describe('event context building', () => { diff --git a/packages/warden/src/action/workflow/pr-workflow.ts b/packages/warden/src/action/workflow/pr-workflow.ts index 879668db2..c89f61e76 100644 --- a/packages/warden/src/action/workflow/pr-workflow.ts +++ b/packages/warden/src/action/workflow/pr-workflow.ts @@ -96,6 +96,7 @@ interface InitResult { matchedTriggers: ResolvedTrigger[]; skippedTriggers: ResolvedTrigger[]; skipCoreCheck?: SkippedCoreCheck; + postChecks: boolean; } interface GitHubSetupResult { @@ -163,8 +164,8 @@ function reportsPullRequestCheck(trigger: ResolvedTrigger, context: EventContext ); } -function checkOptionsForPullRequest(context: EventContext): CheckOptions | undefined { - if (!context.pullRequest) { +function checkOptionsForPullRequest(context: EventContext, postChecks: boolean): CheckOptions | undefined { + if (!context.pullRequest || !postChecks) { return undefined; } @@ -329,6 +330,7 @@ async function initializeWorkflow( layered.config.runner?.concurrency; auxiliaryOptions = resolveWorkflowAuxiliaryOptions(layered); skillRootsByName = buildSkillRootsByName(repoPath, layered, inputs.baseSkillRoot); + const postChecks = layered.config.defaults?.postChecks ?? inputs.postChecks; const resolvedTriggers = resolveLayeredSkillConfigs(layered, undefined, skillRootsByName); const matchedTriggers = resolvedTriggers.filter((t) => matchTrigger(t, context, 'github')); const skippedTriggers = resolvedTriggers.filter( @@ -345,7 +347,7 @@ async function initializeWorkflow( console.log('No triggers matched for this event'); } - return { context, runnerConcurrency, auxiliaryOptions, matchedTriggers, skippedTriggers }; + return { context, runnerConcurrency, auxiliaryOptions, matchedTriggers, skippedTriggers, postChecks }; } catch (error) { if ( error instanceof ConfigLoadError && @@ -364,6 +366,7 @@ async function initializeWorkflow( title: 'No warden.toml found', message, }, + postChecks: inputs.postChecks, }; } throw error; @@ -413,7 +416,8 @@ async function fetchPreviousReviewInfo( */ async function setupGitHubState( octokit: Octokit, - context: EventContext + context: EventContext, + postChecks: boolean ): Promise { if (!context.pullRequest) { return { previousReviewInfo: null }; @@ -423,17 +427,16 @@ async function setupGitHubState( let previousReviewInfo: BotReviewInfo | null = null; // Create core warden check - try { - const coreCheck = await createCoreCheck(octokit, { - owner: context.repository.owner, - repo: context.repository.name, - headSha: context.pullRequest.headSha, - }); - coreCheckId = coreCheck.checkRunId; - logAction(`Created core check: ${coreCheck.url}`); - } catch (error) { - Sentry.captureException(error, { tags: { operation: 'create_core_check' } }); - warnAction(`Failed to create core check: ${error}`); + const checkOptions = checkOptionsForPullRequest(context, postChecks); + if (checkOptions) { + try { + const coreCheck = await createCoreCheck(octokit, checkOptions); + coreCheckId = coreCheck.checkRunId; + logAction(`Created core check: ${coreCheck.url}`); + } catch (error) { + Sentry.captureException(error, { tags: { operation: 'create_core_check' } }); + warnAction(`Failed to create core check: ${error}`); + } } previousReviewInfo = await fetchPreviousReviewInfo(octokit, context); @@ -451,9 +454,10 @@ async function setupGitHubState( */ function createTriggerCheckReporter( octokit: Octokit, - context: EventContext + context: EventContext, + postChecks: boolean ): TriggerCheckReporter | undefined { - const checkOptions = checkOptionsForPullRequest(context); + const checkOptions = checkOptionsForPullRequest(context, postChecks); if (!checkOptions) { return undefined; } @@ -980,7 +984,8 @@ async function finalizeWorkflow( failureReasons: string[], canResolveStale: boolean, gate: ReviewFeedbackGate, - triggerErrors: string[] + triggerErrors: string[], + postChecks: boolean ): Promise { await dismissPreviousReviewIfResolved( octokit, @@ -1006,21 +1011,21 @@ async function finalizeWorkflow( } // Update core check with overall summary - if (coreCheckId && context.pullRequest) { - try { - const summaryData = buildCoreSummaryData(results, reports); - const coreConclusion = determineCoreConclusion( - shouldFailAction || triggerErrors.length > 0, - outputs.findingsCount - ); + if (coreCheckId) { + const checkOptions = checkOptionsForPullRequest(context, postChecks); + if (checkOptions) { + try { + const summaryData = buildCoreSummaryData(results, reports); + const coreConclusion = determineCoreConclusion( + shouldFailAction || triggerErrors.length > 0, + outputs.findingsCount + ); - await updateCoreCheck(octokit, coreCheckId, summaryData, coreConclusion, { - owner: context.repository.owner, - repo: context.repository.name, - }); - } catch (error) { - Sentry.captureException(error, { tags: { operation: 'update_core_check' } }); - warnAction(`Failed to update core check: ${error}`); + await updateCoreCheck(octokit, coreCheckId, summaryData, coreConclusion, checkOptions); + } catch (error) { + Sentry.captureException(error, { tags: { operation: 'update_core_check' } }); + warnAction(`Failed to update core check: ${error}`); + } } } @@ -1036,9 +1041,10 @@ async function completeSkippedCoreCheck( octokit: Octokit, context: EventContext, coreCheckId: number | undefined, - skipped: SkippedCoreCheck + skipped: SkippedCoreCheck, + postChecks: boolean ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!coreCheckId || !options) { return; } @@ -1065,9 +1071,10 @@ async function completeSkippedCoreCheck( async function completeSkippedSkillChecks( octokit: Octokit, context: EventContext, - skippedTriggers: ResolvedTrigger[] + skippedTriggers: ResolvedTrigger[], + postChecks: boolean ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!options || skippedTriggers.length === 0) { return; } @@ -1114,9 +1121,10 @@ async function failUndispatchedSkillChecks( octokit: Octokit, context: EventContext, triggers: ResolvedTrigger[], - error: unknown + error: unknown, + postChecks: boolean ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!options || triggers.length === 0) { return; } @@ -1146,9 +1154,10 @@ async function failCoreCheck( octokit: Octokit, context: EventContext, coreCheckId: number | undefined, - error: unknown + error: unknown, + postChecks: boolean ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!coreCheckId || !options) { return; } @@ -1177,12 +1186,13 @@ async function runOrFailCore( octokit: Octokit, context: EventContext, coreCheckId: number | undefined, + postChecks: boolean, operation: () => Promise ): Promise { try { return await operation(); } catch (error) { - await failCoreCheck(octokit, context, coreCheckId, error); + await failCoreCheck(octokit, context, coreCheckId, error, postChecks); throw error; } } @@ -1429,9 +1439,10 @@ function withRenderedReviewResult(result: TriggerResult): TriggerResult { async function createCompletedSkillChecksForReport( octokit: Octokit, context: EventContext, - results: TriggerResult[] + results: TriggerResult[], + postChecks: boolean ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!options) { return results.map(withRenderedReviewResult); } @@ -1469,9 +1480,10 @@ async function createCompletedSkillChecksForReport( async function createCompletedSkippedSkillChecks( octokit: Octokit, context: EventContext, - skippedTriggers: ResolvedTrigger[] + skippedTriggers: ResolvedTrigger[], + postChecks: boolean ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!options || skippedTriggers.length === 0) { return; } @@ -1507,10 +1519,11 @@ async function createCompletedCoreCheckForReport( reports: SkillReport[], shouldFailAction: boolean, outputs: { findingsCount: number }, + postChecks: boolean, overrides: Partial = {}, conclusion?: 'success' | 'failure' | 'neutral' ): Promise { - const options = checkOptionsForPullRequest(context); + const options = checkOptionsForPullRequest(context, postChecks); if (!options) { return; } @@ -1532,7 +1545,8 @@ async function createCompletedCoreCheckForReport( async function createFailedCoreCheckForReport( octokit: Octokit, context: EventContext, - error: unknown + error: unknown, + postChecks: boolean ): Promise { const errorMessage = error instanceof Error ? error.message : String(error); @@ -1544,6 +1558,7 @@ async function createFailedCoreCheckForReport( [], true, { findingsCount: 0 }, + postChecks, { title: 'Warden failed', message: `Error: ${errorMessage}`, @@ -1572,7 +1587,7 @@ async function finalizeReportWorkflow( canResolveStale: boolean, gate: ReviewFeedbackGate, triggerErrors: string[], - options: { failOnWriteError?: boolean } = {} + options: { failOnWriteError?: boolean; postChecks?: boolean } = {} ): Promise { await dismissPreviousReviewIfResolved( octokit, @@ -1602,7 +1617,8 @@ async function finalizeReportWorkflow( results, reports, shouldFailAction || triggerErrors.length > 0, - outputs + outputs, + options.postChecks ?? true ); if (shouldFailAction) { @@ -1774,6 +1790,7 @@ async function runReportMode( matchedTriggers, skippedTriggers, skipCoreCheck, + postChecks, } = initResult; const findingsOutput = readFindingsFile(inputs.findingsFile, repoPath); validateFindingsMatchContext(findingsOutput, context); @@ -1786,7 +1803,7 @@ async function runReportMode( try { results = buildReportModeResults(findingsOutput, matchedTriggers, inputs); - await createCompletedSkippedSkillChecks(octokit, context, skippedTriggers); + await createCompletedSkippedSkillChecks(octokit, context, skippedTriggers, postChecks); if (skipCoreCheck) { const outputs = { findingsCount: 0, highCount: 0, summary: skipCoreCheck.title }; @@ -1804,6 +1821,7 @@ async function runReportMode( [], false, outputs, + postChecks, { title: skipCoreCheck.title, message: skipCoreCheck.message, @@ -1839,6 +1857,7 @@ async function runReportMode( [], false, outputs, + postChecks, { title: 'No triggers matched', message: 'No triggers matched for this event.', @@ -1849,7 +1868,7 @@ async function runReportMode( return; } - results = await createCompletedSkillChecksForReport(octokit, context, results); + results = await createCompletedSkillChecksForReport(octokit, context, results, postChecks); previousReviewInfo = await fetchPreviousReviewInfo(octokit, context); if (previousReviewInfo) { @@ -1899,13 +1918,13 @@ async function runReportMode( canResolveStale, gate, triggerErrors, - { failOnWriteError: true }, + { failOnWriteError: true, postChecks }, ); } catch (error) { if (error instanceof ActionFailedError) { throw error; } - await createFailedCoreCheckForReport(octokit, context, error); + await createFailedCoreCheckForReport(octokit, context, error, postChecks); throw error; } @@ -1941,6 +1960,7 @@ export async function runPRWorkflow( matchedTriggers, skippedTriggers, skipCoreCheck, + postChecks, } = initResult; span.setAttribute('warden.trigger.count', matchedTriggers.length); @@ -1978,10 +1998,10 @@ export async function runPRWorkflow( const { coreCheckId, previousReviewInfo } = await Sentry.startSpan( { op: 'workflow.setup', name: 'setup github state' }, - () => setupGitHubState(octokit, context), + () => setupGitHubState(octokit, context, postChecks), ); - await completeSkippedSkillChecks(octokit, context, skippedTriggers); + await completeSkippedSkillChecks(octokit, context, skippedTriggers, postChecks); if (skipCoreCheck) { setOutput('findings-count', 0); @@ -1992,12 +2012,12 @@ export async function runPRWorkflow( } catch (error) { warnAction(`Failed to write findings output: ${error}`); } - await completeSkippedCoreCheck(octokit, context, coreCheckId, skipCoreCheck); + await completeSkippedCoreCheck(octokit, context, coreCheckId, skipCoreCheck, postChecks); return; } if (matchedTriggers.length === 0) { - await runOrFailCore(octokit, context, coreCheckId, async () => { + await runOrFailCore(octokit, context, coreCheckId, postChecks, async () => { const cleanupFindingObservations = await cleanupOrphanedComments( octokit, context, @@ -2015,7 +2035,7 @@ export async function runPRWorkflow( await completeSkippedCoreCheck(octokit, context, coreCheckId, { title: 'No triggers matched', message: 'No triggers matched for this event.', - }); + }, postChecks); }); return; } @@ -2029,12 +2049,12 @@ export async function runPRWorkflow( attributes: { 'warden.trigger.count': matchedTriggers.length }, }, () => executeAllTriggers(matchedTriggers, context, runnerConcurrency, inputs, { - checks: createTriggerCheckReporter(octokit, context), + checks: createTriggerCheckReporter(octokit, context, postChecks), }), ); } catch (error) { - await failUndispatchedSkillChecks(octokit, context, matchedTriggers, error); - await failCoreCheck(octokit, context, coreCheckId, error); + await failUndispatchedSkillChecks(octokit, context, matchedTriggers, error, postChecks); + await failCoreCheck(octokit, context, coreCheckId, error, postChecks); throw error; } @@ -2043,6 +2063,7 @@ export async function runPRWorkflow( octokit, context, coreCheckId, + postChecks, () => Sentry.startSpan( { op: 'workflow.review', name: 'post reviews' }, () => postReviewsAndTrackFailures(octokit, context, results, inputs, auxiliaryOptions, gate), @@ -2058,6 +2079,7 @@ export async function runPRWorkflow( octokit, context, coreCheckId, + postChecks, () => Sentry.startSpan( { op: 'workflow.resolve', name: 'resolve stale comments' }, async (resolveSpan) => { @@ -2088,6 +2110,7 @@ export async function runPRWorkflow( canResolveStale, gate, triggerErrors, + postChecks, ); handleTriggerErrors(triggerErrors, matchedTriggers.length); diff --git a/packages/warden/src/action/workflow/schedule.test.ts b/packages/warden/src/action/workflow/schedule.test.ts index f8943cbd1..4c137bd3b 100644 --- a/packages/warden/src/action/workflow/schedule.test.ts +++ b/packages/warden/src/action/workflow/schedule.test.ts @@ -136,6 +136,7 @@ function createDefaultInputs(overrides: Partial = {}): ActionInput mode: 'run', configPath: 'warden.toml', maxFindings: 50, + postChecks: true, parallel: 2, ...overrides, }; diff --git a/packages/warden/src/config/loader.test.ts b/packages/warden/src/config/loader.test.ts index ef2d1466a..033d22924 100644 --- a/packages/warden/src/config/loader.test.ts +++ b/packages/warden/src/config/loader.test.ts @@ -1626,6 +1626,47 @@ describe('requestChanges and failCheck config', () => { }); }); +describe('postChecks config', () => { + it('accepts postChecks in defaults', () => { + const config = { + version: 1, + defaults: { postChecks: false }, + skills: [], + }; + + const result = WardenConfigSchema.safeParse(config); + expect(result.success).toBe(true); + expect(result.data?.defaults?.postChecks).toBe(false); + }); + + it('rejects non-boolean postChecks', () => { + const config = { + version: 1, + defaults: { postChecks: 'yes' }, + skills: [], + }; + + const result = WardenConfigSchema.safeParse(config); + expect(result.success).toBe(false); + }); + + it('repo config overrides base config for postChecks on merge', () => { + const baseConfig: WardenConfig = { + version: 1, + defaults: { postChecks: false }, + skills: [], + }; + const repoConfig: WardenConfig = { + version: 1, + defaults: { postChecks: true }, + skills: [], + }; + + const merged = mergeWardenConfigs(baseConfig, repoConfig); + expect(merged.defaults?.postChecks).toBe(true); + }); +}); + describe('defaults.ignorePaths config', () => { it('accepts ignorePaths in defaults', () => { const config = { diff --git a/packages/warden/src/config/schema.ts b/packages/warden/src/config/schema.ts index 1077f8e8a..afcecaeb1 100644 --- a/packages/warden/src/config/schema.ts +++ b/packages/warden/src/config/schema.ts @@ -233,6 +233,8 @@ export const DefaultsSchema = z.object({ requestChanges: z.boolean().optional(), /** Fail the check run when findings exceed failOn. Default: false */ failCheck: z.boolean().optional(), + /** Create/update GitHub Check runs (core + per-skill). Default: true */ + postChecks: z.boolean().optional(), /** Default model for all skills (e.g., 'openai/gpt-5.5') */ model: z.string().optional(), /** Maximum agentic turns (API round-trips) per hunk analysis. Default: 50 */ From d6064f783480f3b5e109b9137df6a8c8c42d4265 Mon Sep 17 00:00:00 2001 From: Caleb Aston Date: Tue, 28 Jul 2026 14:51:59 -0600 Subject: [PATCH 2/4] Fix postChecks precedence: org base config must win over repo config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit initializeWorkflow already treats runnerConcurrency and auxiliaryOptions as enforced by the org base config, with repo config only filling in what the base omits (documented inline right above where postChecks was resolved). postChecks is the same class of workflow-level setting, but was reading the already-merged (repo-wins) config instead, so a repo's own warden.toml could silently defeat an org policy meant to turn checks off fleet-wide. Proved by reverting the fix and watching the new org-base-config test fail exactly as expected. Also drops the dead `options.postChecks ?? true` fallback in finalizeReportWorkflow — its only caller always supplies postChecks, so the fallback could never actually run. Co-Authored-By: Claude Sonnet 5 --- .../.warden-org/warden.toml | 3 +++ .../layered-auxiliary-model/warden.toml | 3 +++ .../src/action/workflow/pr-workflow.test.ts | 18 ++++++++++++++++++ .../warden/src/action/workflow/pr-workflow.ts | 13 ++++++++++--- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/.warden-org/warden.toml b/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/.warden-org/warden.toml index 6901655d9..4ce598f70 100644 --- a/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/.warden-org/warden.toml +++ b/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/.warden-org/warden.toml @@ -1,5 +1,8 @@ version = 1 +[defaults] +postChecks = false + [defaults.auxiliary] model = "anthropic/org-aux-model" maxRetries = 7 diff --git a/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/warden.toml b/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/warden.toml index 0e2a04c6c..0dde2313f 100644 --- a/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/warden.toml +++ b/packages/warden/src/action/workflow/__fixtures__/layered-auxiliary-model/warden.toml @@ -1,5 +1,8 @@ version = 1 +[defaults] +postChecks = true + [defaults.auxiliary] model = "anthropic/repo-aux-model" maxRetries = 2 diff --git a/packages/warden/src/action/workflow/pr-workflow.test.ts b/packages/warden/src/action/workflow/pr-workflow.test.ts index 6d209aa94..7ba02240d 100644 --- a/packages/warden/src/action/workflow/pr-workflow.test.ts +++ b/packages/warden/src/action/workflow/pr-workflow.test.ts @@ -2008,6 +2008,24 @@ describe('runPRWorkflow', () => { }) ); }); + + it('org base config postChecks=false wins over a repo config postChecks=true', async () => { + mockRunSkillTask.mockResolvedValue({ name: 'test-trigger', report: createSkillReport() }); + + await runPRWorkflow( + mockOctokit, + createDefaultInputs({ + baseConfigPath: '.warden-org/warden.toml', + baseSkillRoot: '.warden-org', + }), + 'pull_request', + EVENT_PAYLOAD_PATH, + LAYERED_AUXILIARY_MODEL_FIXTURES_DIR + ); + + expect(mockOctokit.checks.create).not.toHaveBeenCalled(); + expect(mockOctokit.checks.update).not.toHaveBeenCalled(); + }); }); describe('event context building', () => { diff --git a/packages/warden/src/action/workflow/pr-workflow.ts b/packages/warden/src/action/workflow/pr-workflow.ts index c89f61e76..f5e826899 100644 --- a/packages/warden/src/action/workflow/pr-workflow.ts +++ b/packages/warden/src/action/workflow/pr-workflow.ts @@ -330,7 +330,14 @@ async function initializeWorkflow( layered.config.runner?.concurrency; auxiliaryOptions = resolveWorkflowAuxiliaryOptions(layered); skillRootsByName = buildSkillRootsByName(repoPath, layered, inputs.baseSkillRoot); - const postChecks = layered.config.defaults?.postChecks ?? inputs.postChecks; + // Same enforced-baseline precedence as runnerConcurrency/auxiliaryOptions above: + // this is a workflow-level setting, not a per-trigger one, so the org base + // config wins over the repo config. + const postChecks = + layered.baseConfig?.defaults?.postChecks ?? + layered.repoConfig?.defaults?.postChecks ?? + layered.config.defaults?.postChecks ?? + inputs.postChecks; const resolvedTriggers = resolveLayeredSkillConfigs(layered, undefined, skillRootsByName); const matchedTriggers = resolvedTriggers.filter((t) => matchTrigger(t, context, 'github')); const skippedTriggers = resolvedTriggers.filter( @@ -1587,7 +1594,7 @@ async function finalizeReportWorkflow( canResolveStale: boolean, gate: ReviewFeedbackGate, triggerErrors: string[], - options: { failOnWriteError?: boolean; postChecks?: boolean } = {} + options: { failOnWriteError?: boolean; postChecks: boolean } ): Promise { await dismissPreviousReviewIfResolved( octokit, @@ -1618,7 +1625,7 @@ async function finalizeReportWorkflow( reports, shouldFailAction || triggerErrors.length > 0, outputs, - options.postChecks ?? true + options.postChecks ); if (shouldFailAction) { From 000d974b796e9d0c1213835fe92d924417216fbc Mon Sep 17 00:00:00 2001 From: Caleb Aston Date: Tue, 28 Jul 2026 16:17:17 -0600 Subject: [PATCH 3/4] docs: Document post-checks and warn against required-check conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fight-me review of this PR caught that post-checks had no docs entry in either the workflow guide or llms.txt, and more importantly that the guide's own Required Status Checks section recommends requiring the warden check — setting post-checks: false on such a repo would silently stop that check from ever running again, permanently blocking merges through the normal GitHub UI. Documents the input in both places and adds an explicit warning against that combination. Co-Authored-By: Claude Sonnet 5 --- packages/docs/public/llms.txt | 1 + packages/docs/src/content/docs/github/workflow.mdx | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index db6be7eba..54a86d415 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -604,6 +604,7 @@ jobs: | `max-findings` | `50` | Maximum findings to report | | `request-changes` | `false` | Whether to request changes on PR reviews | | `fail-check` | `false` | Whether to fail the check run | +| `post-checks` | `true` | Whether to create/update the core `warden` check and per-skill checks. Set `false` to post only PR review comments — do not combine with a required Warden status check. | | `parallel` | `5` | Maximum concurrent trigger executions | ### Split Analyze and Report diff --git a/packages/docs/src/content/docs/github/workflow.mdx b/packages/docs/src/content/docs/github/workflow.mdx index ba4103a6c..75ea3bcd5 100644 --- a/packages/docs/src/content/docs/github/workflow.mdx +++ b/packages/docs/src/content/docs/github/workflow.mdx @@ -172,6 +172,10 @@ workflows. Let Warden start, then let `warden.toml` decide which triggers match. For each configured pull request trigger, Warden creates a check run. Triggers that do not actually run for the current event complete as neutral. +Do not set `post-checks: false` on a repo where `warden` or a per-skill check +is a required status check — Warden stops creating that check entirely, so it +never reports and the required check blocks every PR indefinitely. + ## Action Inputs
@@ -259,6 +263,13 @@ that do not actually run for the current event complete as neutral.
Whether to fail the check run. Default: false.
+
+
+ post-checks + boolean +
+
Whether to create/update the core warden check and per-skill checks. Set to false to post only PR review comments. Default: true. See Required Status Checks before disabling this on a repo with a required Warden check.
+
parallel From ae1366507ce8c57e539798d178d4ad11a6dae9ac Mon Sep 17 00:00:00 2001 From: Caleb Aston Date: Thu, 30 Jul 2026 10:16:39 -0600 Subject: [PATCH 4/4] Retrigger CI Co-Authored-By: Claude Sonnet 5