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/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 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/__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/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..7ba02240d 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,126 @@ 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, + }) + ); + }); + + 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 879668db2..f5e826899 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,14 @@ async function initializeWorkflow( layered.config.runner?.concurrency; auxiliaryOptions = resolveWorkflowAuxiliaryOptions(layered); skillRootsByName = buildSkillRootsByName(repoPath, layered, inputs.baseSkillRoot); + // 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( @@ -345,7 +354,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 +373,7 @@ async function initializeWorkflow( title: 'No warden.toml found', message, }, + postChecks: inputs.postChecks, }; } throw error; @@ -413,7 +423,8 @@ async function fetchPreviousReviewInfo( */ async function setupGitHubState( octokit: Octokit, - context: EventContext + context: EventContext, + postChecks: boolean ): Promise { if (!context.pullRequest) { return { previousReviewInfo: null }; @@ -423,17 +434,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 +461,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 +991,8 @@ async function finalizeWorkflow( failureReasons: string[], canResolveStale: boolean, gate: ReviewFeedbackGate, - triggerErrors: string[] + triggerErrors: string[], + postChecks: boolean ): Promise { await dismissPreviousReviewIfResolved( octokit, @@ -1006,21 +1018,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 +1048,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 +1078,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 +1128,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 +1161,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 +1193,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 +1446,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 +1487,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 +1526,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 +1552,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 +1565,7 @@ async function createFailedCoreCheckForReport( [], true, { findingsCount: 0 }, + postChecks, { title: 'Warden failed', message: `Error: ${errorMessage}`, @@ -1572,7 +1594,7 @@ async function finalizeReportWorkflow( canResolveStale: boolean, gate: ReviewFeedbackGate, triggerErrors: string[], - options: { failOnWriteError?: boolean } = {} + options: { failOnWriteError?: boolean; postChecks: boolean } ): Promise { await dismissPreviousReviewIfResolved( octokit, @@ -1602,7 +1624,8 @@ async function finalizeReportWorkflow( results, reports, shouldFailAction || triggerErrors.length > 0, - outputs + outputs, + options.postChecks ); if (shouldFailAction) { @@ -1774,6 +1797,7 @@ async function runReportMode( matchedTriggers, skippedTriggers, skipCoreCheck, + postChecks, } = initResult; const findingsOutput = readFindingsFile(inputs.findingsFile, repoPath); validateFindingsMatchContext(findingsOutput, context); @@ -1786,7 +1810,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 +1828,7 @@ async function runReportMode( [], false, outputs, + postChecks, { title: skipCoreCheck.title, message: skipCoreCheck.message, @@ -1839,6 +1864,7 @@ async function runReportMode( [], false, outputs, + postChecks, { title: 'No triggers matched', message: 'No triggers matched for this event.', @@ -1849,7 +1875,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 +1925,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 +1967,7 @@ export async function runPRWorkflow( matchedTriggers, skippedTriggers, skipCoreCheck, + postChecks, } = initResult; span.setAttribute('warden.trigger.count', matchedTriggers.length); @@ -1978,10 +2005,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 +2019,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 +2042,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 +2056,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 +2070,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 +2086,7 @@ export async function runPRWorkflow( octokit, context, coreCheckId, + postChecks, () => Sentry.startSpan( { op: 'workflow.resolve', name: 'resolve stale comments' }, async (resolveSpan) => { @@ -2088,6 +2117,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 */