Skip to content
Draft
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 action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ branding:
color: 'purple'

inputs:
provider:
description: 'Provider to use (claude or pi). Overrides WARDEN_PROVIDER when set.'
required: false
anthropic-api-key:
description: 'Anthropic API key (sk-ant-...) or OAuth token. Can also be set via ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN env vars.'
required: false
Expand Down Expand Up @@ -61,6 +64,7 @@ runs:
using: 'composite'
steps:
- name: Install Claude Code CLI
if: ${{ inputs.provider != 'pi' && env.WARDEN_PROVIDER != 'pi' }}
shell: bash
run: |
CLAUDE_CODE_VERSION="2.1.32"
Expand All @@ -84,6 +88,7 @@ runs:
id: warden
shell: bash
env:
INPUT_PROVIDER: ${{ inputs.provider }}
INPUT_ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }}
INPUT_GITHUB_TOKEN: ${{ inputs.github-token }}
INPUT_CONFIG_PATH: ${{ inputs.config-path }}
Expand All @@ -93,5 +98,6 @@ runs:
INPUT_REQUEST_CHANGES: ${{ inputs.request-changes }}
INPUT_FAIL_CHECK: ${{ inputs.fail-check }}
INPUT_PARALLEL: ${{ inputs.parallel }}
WARDEN_PROVIDER: ${{ inputs.provider || env.WARDEN_PROVIDER }}
CLAUDE_CODE_PATH: ${{ env.HOME }}/.local/bin/claude
run: node ${{ github.action_path }}/dist/action/index.js
5 changes: 4 additions & 1 deletion src/action/fix-evaluation/judge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { FixJudgeVerdictSchema } from './types.js';
import type { FixJudgeResult } from './types.js';
import { fetchFileContent, fetchFileLines } from './github.js';
import type { Provider } from '../../config/schema.js';

export interface FixJudgeInput {
comment: ExistingComment;
Expand Down Expand Up @@ -212,7 +213,8 @@
input: FixJudgeInput,
context: FixJudgeContext,
apiKey: string,
maxRetries?: number
maxRetries?: number,
provider: Provider = 'claude'

Check failure on line 217 in src/action/fix-evaluation/judge.ts

View check run for this annotation

@sentry/warden / warden: find-warden-bugs

Provider parameter not threaded through fix evaluation call chain

The `evaluateFix` function adds a `provider` parameter with default `'claude'`, but the caller in `src/action/fix-evaluation/index.ts` (line 206) doesn't pass it, and `evaluateFixAttempts` doesn't accept a provider from its callers. This means fix evaluation will always use Claude regardless of the configured provider, breaking the PR's goal of consistent provider selection across CLI and GitHub Action paths.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider parameter not threaded through fix evaluation call chain

The evaluateFix function adds a provider parameter with default 'claude', but the caller in src/action/fix-evaluation/index.ts (line 206) doesn't pass it, and evaluateFixAttempts doesn't accept a provider from its callers. This means fix evaluation will always use Claude regardless of the configured provider, breaking the PR's goal of consistent provider selection across CLI and GitHub Action paths.

Verification

Verified by reading: (1) judge.ts lines 212-237 showing evaluateFix signature with provider default, (2) index.ts lines 206-211 showing evaluateFix called without provider argument, (3) index.ts line 82-89 showing evaluateFixAttempts signature lacks provider parameter, (4) pr-workflow.ts lines 395-407 showing evaluateFixAttempts called without provider. The provider configuration set in warden.toml or action inputs is not threaded through this call chain.

Also found at 1 additional location
  • src/action/inputs.ts:91-91

Identified by Warden find-warden-bugs · 5LW-JGD

): Promise<FixJudgeResult> {
const fallback: FixJudgeResult = {
verdict: { status: 'not_attempted', reasoning: 'Evaluation failed' },
Expand All @@ -225,6 +227,7 @@

const result = await callHaikuWithTools({
apiKey,
provider,
prompt,
schema: FixJudgeVerdictSchema,
tools: TOOL_DEFINITIONS,
Expand Down
12 changes: 10 additions & 2 deletions src/action/inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,19 @@ describe('parseActionInputs', () => {
expect(inputs.anthropicApiKey).toBe('');
});

it('throws when no auth token is found', () => {
it('allows empty auth tokens (validated later by provider)', () => {
delete process.env['ANTHROPIC_API_KEY'];
delete process.env['WARDEN_ANTHROPIC_API_KEY'];
delete process.env['CLAUDE_CODE_OAUTH_TOKEN'];
expect(() => parseActionInputs()).toThrow('Authentication not found');
const inputs = parseActionInputs();
expect(inputs.anthropicApiKey).toBe('');
expect(inputs.oauthToken).toBe('');
});

it('parses provider from INPUT_PROVIDER', () => {
process.env['INPUT_PROVIDER'] = 'pi';
const inputs = parseActionInputs();
expect(inputs.provider).toBe('pi');
});
});

Expand Down
44 changes: 30 additions & 14 deletions src/action/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
import { SeverityThresholdSchema } from '../types/index.js';
import type { SeverityThreshold } from '../types/index.js';
import { DEFAULT_CONCURRENCY } from '../utils/index.js';
import type { Provider } from '../config/schema.js';

// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------

export interface ActionInputs {
/** Optional provider override (defaults to config/env) */
provider?: Provider;
/** API key for Anthropic API (empty if using OAuth) */
anthropicApiKey: string;
/** API key for Pi provider */
piApiKey?: string;
/** OAuth token for Claude Code (empty if using API key) */
oauthToken: string;
githubToken: string;
Expand Down Expand Up @@ -58,31 +63,32 @@
return undefined;
}

function parseProviderInput(value: string): Provider | undefined {
return value === 'claude' || value === 'pi' ? value : undefined;
}

/**
* Parse action inputs from the GitHub Actions environment.
* Throws if required inputs are missing.
*/
export function parseActionInputs(): ActionInputs {
// Check for auth token: supports both API keys and OAuth tokens
const providerInput = getInput('provider') || process.env['WARDEN_PROVIDER'] || '';
const provider = parseProviderInput(providerInput);

// Claude auth token: supports both API keys and OAuth tokens
// Priority: input > WARDEN_ANTHROPIC_API_KEY > ANTHROPIC_API_KEY > CLAUDE_CODE_OAUTH_TOKEN
const authToken =
const claudeAuthToken =
getInput('anthropic-api-key') ||
process.env['WARDEN_ANTHROPIC_API_KEY'] ||
process.env['ANTHROPIC_API_KEY'] ||
process.env['CLAUDE_CODE_OAUTH_TOKEN'] ||
'';

if (!authToken) {
throw new Error(
'Authentication not found. Provide an API key via anthropic-api-key input, ' +
'ANTHROPIC_API_KEY env var, or OAuth token via CLAUDE_CODE_OAUTH_TOKEN env var.'
);
}

// Detect token type: OAuth tokens start with 'sk-ant-oat', API keys are other 'sk-ant-' prefixes
const isOAuthToken = authToken.startsWith('sk-ant-oat');
const anthropicApiKey = isOAuthToken ? '' : authToken;
const oauthToken = isOAuthToken ? authToken : '';
const isOAuthToken = claudeAuthToken.startsWith('sk-ant-oat');
const anthropicApiKey = isOAuthToken ? '' : claudeAuthToken;
const oauthToken = isOAuthToken ? claudeAuthToken : '';
const piApiKey = process.env['WARDEN_PI_API_KEY'] || '';

Check failure on line 91 in src/action/inputs.ts

View check run for this annotation

@sentry/warden / warden: find-warden-bugs

[5LW-JGD] Provider parameter not threaded through fix evaluation call chain (additional location)

The `evaluateFix` function adds a `provider` parameter with default `'claude'`, but the caller in `src/action/fix-evaluation/index.ts` (line 206) doesn't pass it, and `evaluateFixAttempts` doesn't accept a provider from its callers. This means fix evaluation will always use Claude regardless of the configured provider, breaking the PR's goal of consistent provider selection across CLI and GitHub Action paths.

const failOnInput = getInput('fail-on');
const failOn = SeverityThresholdSchema.safeParse(failOnInput).success
Expand All @@ -101,7 +107,9 @@
const failCheck = parseBooleanInput(getInput('fail-check'));

return {
provider,
anthropicApiKey,
piApiKey,
oauthToken,
githubToken: getInput('github-token') || process.env['GITHUB_TOKEN'] || '',
configPath: getInput('config-path') || 'warden.toml',
Expand Down Expand Up @@ -129,10 +137,18 @@
* Sets appropriate env vars based on token type (API key vs OAuth).
*/
export function setupAuthEnv(inputs: ActionInputs): void {
if (inputs.provider) {
process.env['WARDEN_PROVIDER'] = inputs.provider;
}
if (inputs.piApiKey) {
process.env['WARDEN_PI_API_KEY'] = inputs.piApiKey;
}
if (inputs.oauthToken) {
process.env['CLAUDE_CODE_OAUTH_TOKEN'] = inputs.oauthToken;
} else {
process.env['WARDEN_ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
process.env['ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
if (inputs.anthropicApiKey) {
process.env['WARDEN_ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
process.env['ANTHROPIC_API_KEY'] = inputs.anthropicApiKey;
}
}
}
3 changes: 2 additions & 1 deletion src/action/triggers/executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ describe('executeTrigger', () => {
octokit: mockOctokit,
context: mockContext,
config: mockConfig,
anthropicApiKey: 'test-key',
apiKey: 'test-key',
provider: 'claude',
claudePath: '/test/claude',
globalMaxFindings: 10,
};
Expand Down
11 changes: 7 additions & 4 deletions src/action/triggers/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Sentry } from '../../sentry.js';
import { ActionFailedError } from '../workflow/base.js';
import type { ResolvedTrigger } from '../../config/loader.js';
import type { WardenConfig } from '../../config/schema.js';
import type { Provider } from '../../config/schema.js';
import type { EventContext, SkillReport, SeverityThreshold, ConfidenceThreshold } from '../../types/index.js';
import type { RenderResult } from '../../output/types.js';
import type { OutputMode } from '../../cli/output/tty.js';
Expand Down Expand Up @@ -43,8 +44,9 @@ export interface TriggerExecutorDeps {
octokit: Octokit;
context: EventContext;
config: WardenConfig;
anthropicApiKey: string;
claudePath: string;
apiKey: string;
provider: Provider;
claudePath?: string;
/** Global fail-on from action inputs (trigger-specific takes precedence) */
globalFailOn?: SeverityThreshold;
/** Global report-on from action inputs (trigger-specific takes precedence) */
Expand Down Expand Up @@ -97,7 +99,7 @@ export async function executeTrigger(
{ op: 'trigger.execute', name: `execute ${trigger.name}` },
async (span) => {
span.setAttribute('skill.name', trigger.skill);
const { octokit, context, config, anthropicApiKey, claudePath } = deps;
const { octokit, context, config, apiKey, claudePath, provider } = deps;

logGroup(`Running trigger: ${trigger.name} (skill: ${trigger.skill})`);

Expand Down Expand Up @@ -134,7 +136,8 @@ export async function executeTrigger(
}),
context: filterContextByPaths(context, trigger.filters),
runnerOptions: {
apiKey: anthropicApiKey,
apiKey,
provider,
model: trigger.model,
maxTurns: trigger.maxTurns,
batchDelayMs: config.defaults?.batchDelayMs,
Expand Down
17 changes: 17 additions & 0 deletions src/action/workflow/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import type { EventContext, SkillReport } from '../../types/index.js';
import { countSeverity } from '../../triggers/matcher.js';
import type { TriggerResult } from '../triggers/executor.js';
import type { Provider, WardenConfig } from '../../config/schema.js';
import type { ActionInputs } from '../inputs.js';

/**
* Sentinel error thrown by setFailed() so the top-level catch handler
Expand Down Expand Up @@ -187,6 +189,21 @@
setOutput('summary', outputs.summary);
}

export function resolveActionProvider(inputs: ActionInputs, config?: WardenConfig): Provider {
if (inputs.provider) return inputs.provider;
const envProvider = process.env['WARDEN_PROVIDER'];
if (envProvider === 'claude' || envProvider === 'pi') return envProvider;
const cfgProvider = config?.defaults?.provider;
if (cfgProvider === 'claude' || cfgProvider === 'pi') return cfgProvider;
return 'claude';
}

Check failure on line 199 in src/action/workflow/base.ts

View check run for this annotation

@sentry/warden / warden: find-warden-bugs

Provider resolution precedence differs between Action and CLI paths

The new `resolveActionProvider()` in `base.ts` uses precedence: inputs.provider &gt; WARDEN_PROVIDER env &gt; config.defaults.provider. However, the CLI's `resolveProvider()` in `main.ts` uses: config.defaults.provider &gt; cliProvider &gt; WARDEN_PROVIDER env. This means the same user configuration produces different provider selection depending on whether Warden runs via GitHub Action or CLI, violating Check 2 (Dual Code Path Desync).

export function getActionProviderApiKey(provider: Provider, inputs: ActionInputs): string {
return provider === 'pi'
? (inputs.piApiKey ?? inputs.anthropicApiKey)
: inputs.anthropicApiKey;
}

// -----------------------------------------------------------------------------
// GitHub API Helpers
// -----------------------------------------------------------------------------
Expand Down
11 changes: 8 additions & 3 deletions src/action/workflow/pr-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import {
setWorkflowOutputs,
getAuthenticatedBotLogin,
writeFindingsOutput,
resolveActionProvider,
getActionProviderApiKey,
} from './base.js';

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -148,7 +150,7 @@ async function initializeWorkflow(
}

// Resolve skills into triggers and match
const resolvedTriggers = resolveSkillConfigs(config);
const resolvedTriggers = resolveSkillConfigs(config, undefined, inputs.provider);
const matchedTriggers = resolvedTriggers.filter((t) => matchTrigger(t, context, 'github'));

if (matchedTriggers.length > 0) {
Expand Down Expand Up @@ -250,7 +252,9 @@ async function executeAllTriggers(
inputs: ActionInputs
): Promise<TriggerResult[]> {
const concurrency = config.runner?.concurrency ?? inputs.parallel;
const claudePath = await findClaudeCodeExecutable();
const provider = resolveActionProvider(inputs, config);
const apiKey = getActionProviderApiKey(provider, inputs);
const claudePath = provider === 'claude' ? await findClaudeCodeExecutable() : undefined;

// Global semaphore gates file-level work across all triggers.
// All triggers launch immediately; the semaphore limits concurrent file analyses.
Expand All @@ -264,7 +268,8 @@ async function executeAllTriggers(
octokit,
context,
config,
anthropicApiKey: inputs.anthropicApiKey,
apiKey,
provider,
claudePath,
globalFailOn: inputs.failOn,
globalReportOn: inputs.reportOn,
Expand Down
11 changes: 8 additions & 3 deletions src/action/workflow/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
handleTriggerErrors,
getDefaultBranchFromAPI,
writeFindingsOutput,
resolveActionProvider,
getActionProviderApiKey,
} from './base.js';

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -67,7 +69,9 @@ export async function runScheduleWorkflow(
}

// Find schedule triggers
const scheduleTriggers = resolveSkillConfigs(config).filter((t) => t.type === 'schedule');
const scheduleTriggers = resolveSkillConfigs(config, undefined, inputs.provider).filter((t) => t.type === 'schedule');
const provider = resolveActionProvider(inputs, config);
const apiKey = getActionProviderApiKey(provider, inputs);
if (scheduleTriggers.length === 0) {
console.log('No schedule triggers configured');
setOutput('findings-count', 0);
Expand Down Expand Up @@ -147,9 +151,10 @@ export async function runScheduleWorkflow(
const skill = await resolveSkillAsync(resolved.skill, repoPath, {
remote: resolved.remote,
});
const claudePath = await findClaudeCodeExecutable();
const claudePath = provider === 'claude' ? await findClaudeCodeExecutable() : undefined;
const report = await runSkill(skill, context, {
apiKey: inputs.anthropicApiKey,
apiKey,
provider,
model: resolved.model,
maxTurns: resolved.maxTurns,
batchDelayMs: config.defaults?.batchDelayMs,
Expand Down
5 changes: 5 additions & 0 deletions src/cli/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ describe('parseCliArgs', () => {
expect(result.options.config).toBe('./custom.toml');
});

it('parses --provider option', () => {
const result = parseCliArgs(['--provider', 'pi']);
expect(result.options.provider).toBe('pi');
});

it('parses --json flag', () => {
const result = parseCliArgs(['--json']);
expect(result.options.json).toBe(true);
Expand Down
5 changes: 5 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const CLIOptionsSchema = z.object({
parallel: z.number().int().positive().optional(),
/** Model to use for analysis (fallback when not set in config) */
model: z.string().optional(),
/** Provider to use for analysis (fallback when not set in config) */
provider: z.enum(['claude', 'pi']).optional(),
// Verbosity options
quiet: z.boolean().default(false),
verbose: z.number().default(0),
Expand Down Expand Up @@ -100,6 +102,7 @@ Options:
--skill <name> Run only this skill (default: run all built-in skills)
--config <path> Path to warden.toml (default: ./warden.toml)
-m, --model <model> Model to use (fallback when not set in config)
--provider <name> Provider to use (claude, pi)
--json Output results as JSON
-o, --output <path> Write full run output to a JSONL file
--fail-on <severity> Exit with code 1 if findings >= severity
Expand Down Expand Up @@ -281,6 +284,7 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs
skill: { type: 'string' },
config: { type: 'string' },
model: { type: 'string', short: 'm' },
provider: { type: 'string' },
json: { type: 'boolean', default: false },
output: { type: 'string', short: 'o' },
'fail-on': { type: 'string' },
Expand Down Expand Up @@ -470,6 +474,7 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs
skill: values.skill,
config: values.config,
model: values.model,
provider: values.provider === 'claude' || values.provider === 'pi' ? values.provider : undefined,
json: values.json,
output: values.output,
failOn: values['fail-on'] as SeverityThreshold | undefined,
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ jobs:
runs-on: ubuntu-latest
env:
WARDEN_MODEL: \${{ secrets.WARDEN_MODEL }}
WARDEN_PROVIDER: \${{ vars.WARDEN_PROVIDER }}
WARDEN_SENTRY_DSN: \${{ secrets.WARDEN_SENTRY_DSN }}
steps:
- uses: actions/checkout@v4
- uses: getsentry/warden@v${majorVersion}
with:
provider: \${{ vars.WARDEN_PROVIDER }}
anthropic-api-key: \${{ secrets.WARDEN_ANTHROPIC_API_KEY }}
`;
}
Expand Down
Loading