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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ concurrency:
| `s3-bucket` | No | — | S3 bucket name (required if `s3-backup` is true) |
| `aws-region` | No | — | AWS region for S3 bucket |
| `skip-cache` | No | `false` | Skip cache restore (useful for debugging) |
| `omo-config` | No | — | Custom oMo configuration JSON (deep-merged) |
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The README inputs table was updated to document omo-config, but this PR also introduces the opencode-config input in action.yaml and it is not documented anywhere in the README. Please add a corresponding row so users can discover and correctly format the new input.

Suggested change
| `omo-config` | No || Custom oMo configuration JSON (deep-merged) |
| `omo-config` | No || Custom oMo configuration JSON (deep-merged) |
| `opencode-config` | No || Custom OpenCode configuration JSON (deep-merged) |

Copilot uses AI. Check for mistakes.
| `opencode-config` | No | — | Custom OpenCode configuration JSON (deep-merged) |

### Action Outputs

Expand Down
14 changes: 12 additions & 2 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,18 @@ inputs:
omo-providers:
description: >-
Comma-separated list of oMo providers to enable. Values: claude, claude-max20,
copilot, gemini, openai, opencode-zen, zai-coding-plan. Default: empty
(uses free OpenCode models)
copilot, gemini, openai, opencode-zen, zai-coding-plan, kimi-for-coding.
Default: empty (uses free OpenCode models)
required: false
opencode-config:
description: >-
JSON object to merge into the OpenCode config (OPENCODE_CONFIG_CONTENT).
Values are merged on top of the autoupdate:false baseline; user values win on conflicts.
required: false
omo-config:
description: >-
Custom oMo configuration JSON. Written before installer runs.
Deep-merged with existing config.
required: false

outputs:
Expand Down
52 changes: 26 additions & 26 deletions dist/main.js

Large diffs are not rendered by default.

60 changes: 54 additions & 6 deletions src/lib/agent/opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,15 @@ describe('executeOpenCode', () => {
agent: 'sisyphus',
model: {providerID: 'anthropic', modelID: 'claude-sonnet-4-20250514'},
timeoutMs: 1800000,
omoProviders: {claude: 'no', copilot: 'no', gemini: 'no', openai: 'no', opencodeZen: 'no', zaiCodingPlan: 'no'},
omoProviders: {
claude: 'no',
copilot: 'no',
gemini: 'no',
openai: 'no',
opencodeZen: 'no',
zaiCodingPlan: 'no',
kimiForCoding: 'no',
},
}

// #when
Expand Down Expand Up @@ -267,7 +275,15 @@ describe('executeOpenCode', () => {
agent: 'sisyphus',
model: null,
timeoutMs: 1800000,
omoProviders: {claude: 'no', copilot: 'no', gemini: 'no', openai: 'no', opencodeZen: 'no', zaiCodingPlan: 'no'},
omoProviders: {
claude: 'no',
copilot: 'no',
gemini: 'no',
openai: 'no',
opencodeZen: 'no',
zaiCodingPlan: 'no',
kimiForCoding: 'no',
},
}

// #when
Expand Down Expand Up @@ -295,7 +311,15 @@ describe('executeOpenCode', () => {
agent: 'sisyphus',
model: null,
timeoutMs: 1800000,
omoProviders: {claude: 'yes', copilot: 'no', gemini: 'no', openai: 'no', opencodeZen: 'no', zaiCodingPlan: 'no'},
omoProviders: {
claude: 'yes',
copilot: 'no',
gemini: 'no',
openai: 'no',
opencodeZen: 'no',
zaiCodingPlan: 'no',
kimiForCoding: 'no',
},
}

// #when
Expand All @@ -320,7 +344,15 @@ describe('executeOpenCode', () => {
agent: 'sisyphus',
model: {providerID: 'openai', modelID: 'gpt-5'},
timeoutMs: 1800000,
omoProviders: {claude: 'yes', copilot: 'no', gemini: 'no', openai: 'yes', opencodeZen: 'no', zaiCodingPlan: 'no'},
omoProviders: {
claude: 'yes',
copilot: 'no',
gemini: 'no',
openai: 'yes',
opencodeZen: 'no',
zaiCodingPlan: 'no',
kimiForCoding: 'no',
},
}

// #when
Expand All @@ -345,7 +377,15 @@ describe('executeOpenCode', () => {
agent: 'CustomAgent',
model: null,
timeoutMs: 1800000,
omoProviders: {claude: 'no', copilot: 'no', gemini: 'no', openai: 'no', opencodeZen: 'no', zaiCodingPlan: 'no'},
omoProviders: {
claude: 'no',
copilot: 'no',
gemini: 'no',
openai: 'no',
opencodeZen: 'no',
zaiCodingPlan: 'no',
kimiForCoding: 'no',
},
}

// #when
Expand All @@ -371,7 +411,15 @@ describe('executeOpenCode', () => {
agent: 'oracle',
model: null,
timeoutMs: 1800000,
omoProviders: {claude: 'no', copilot: 'no', gemini: 'no', openai: 'no', opencodeZen: 'no', zaiCodingPlan: 'no'},
omoProviders: {
claude: 'no',
copilot: 'no',
gemini: 'no',
openai: 'no',
opencodeZen: 'no',
zaiCodingPlan: 'no',
kimiForCoding: 'no',
},
}

// #when
Expand Down
114 changes: 114 additions & 0 deletions src/lib/inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,120 @@ describe('parseActionInputs', () => {
expect(result.success).toBe(false)
expect(!result.success && result.error.message).toContain('Invalid model format')
})

it('returns error for invalid opencode-config (not valid JSON)', () => {
const mockGetInput = core.getInput as ReturnType<typeof vi.fn>

mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'github-token': 'ghp_test123',
'auth-json': '{"anthropic":{"type":"api","key":"sk-ant-test"}}',
'opencode-config': 'not-valid-json',
}
return inputs[name] ?? ''
})

const result = parseActionInputs()

expect(result.success).toBe(false)
expect(!result.success && result.error.message).toContain('opencode-config')
expect(!result.success && result.error.message).toContain('valid JSON')
})

it('returns error when opencode-config is JSON null literal', () => {
const mockGetInput = core.getInput as ReturnType<typeof vi.fn>

mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'github-token': 'ghp_test123',
'auth-json': '{"anthropic":{"type":"api","key":"sk-ant-test"}}',
'opencode-config': 'null',
}
return inputs[name] ?? ''
})

const result = parseActionInputs()

expect(result.success).toBe(false)
expect(!result.success && result.error.message).toContain('opencode-config')
expect(!result.success && result.error.message).toContain('JSON object')
})

it('returns error when opencode-config is a JSON array', () => {
const mockGetInput = core.getInput as ReturnType<typeof vi.fn>

mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'github-token': 'ghp_test123',
'auth-json': '{"anthropic":{"type":"api","key":"sk-ant-test"}}',
'opencode-config': '[1,2,3]',
}
return inputs[name] ?? ''
})

const result = parseActionInputs()

expect(result.success).toBe(false)
expect(!result.success && result.error.message).toContain('opencode-config')
expect(!result.success && result.error.message).toContain('JSON object')
})

it('returns error when opencode-config is a JSON string literal', () => {
const mockGetInput = core.getInput as ReturnType<typeof vi.fn>

mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'github-token': 'ghp_test123',
'auth-json': '{"anthropic":{"type":"api","key":"sk-ant-test"}}',
'opencode-config': '"literal"',
}
return inputs[name] ?? ''
})

const result = parseActionInputs()

expect(result.success).toBe(false)
expect(!result.success && result.error.message).toContain('opencode-config')
expect(!result.success && result.error.message).toContain('JSON object')
})
})

describe('with valid opencode-config', () => {
it('parses valid JSON object in opencode-config', () => {
const mockGetInput = core.getInput as ReturnType<typeof vi.fn>

mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'github-token': 'ghp_test123',
'auth-json': '{"anthropic":{"type":"api","key":"sk-ant-test"}}',
'opencode-config': '{"model": "claude-opus-4", "temperature": 0.7}',
}
return inputs[name] ?? ''
})

const result = parseActionInputs()

expect(result.success).toBe(true)
expect(result.success && result.data.opencodeConfig).toBe('{"model": "claude-opus-4", "temperature": 0.7}')
})

it('sets opencodeConfig to null when empty string', () => {
const mockGetInput = core.getInput as ReturnType<typeof vi.fn>

mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'github-token': 'ghp_test123',
'auth-json': '{"anthropic":{"type":"api","key":"sk-ant-test"}}',
'opencode-config': '',
}
return inputs[name] ?? ''
})

const result = parseActionInputs()

expect(result.success).toBe(true)
expect(result.success && result.data.opencodeConfig).toBe(null)
})
})
})

Expand Down
25 changes: 24 additions & 1 deletion src/lib/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const VALID_OMO_PROVIDERS = [
'openai',
'opencode-zen',
'zai-coding-plan',
'kimi-for-coding',
] as const

type OmoProviderInput = (typeof VALID_OMO_PROVIDERS)[number]
Expand All @@ -81,6 +82,7 @@ function parseOmoProviders(input: string): OmoProviders {
let openai: 'no' | 'yes' = 'no'
let opencodeZen: 'no' | 'yes' = 'no'
let zaiCodingPlan: 'no' | 'yes' = 'no'
let kimiForCoding: 'no' | 'yes' = 'no'

for (const provider of providers) {
if (!VALID_OMO_PROVIDERS.includes(provider as OmoProviderInput)) {
Expand Down Expand Up @@ -109,10 +111,13 @@ function parseOmoProviders(input: string): OmoProviders {
case 'zai-coding-plan':
zaiCodingPlan = 'yes'
break
case 'kimi-for-coding':
kimiForCoding = 'yes'
break
}
}

return {claude, copilot, gemini, openai, opencodeZen, zaiCodingPlan}
return {claude, copilot, gemini, openai, opencodeZen, zaiCodingPlan, kimiForCoding}
}

/**
Expand Down Expand Up @@ -179,6 +184,23 @@ export function parseActionInputs(): Result<ActionInputs, Error> {
const omoProvidersRaw = core.getInput('omo-providers').trim()
const omoProviders = parseOmoProviders(omoProvidersRaw.length > 0 ? omoProvidersRaw : DEFAULT_OMO_PROVIDERS)

const opencodeConfigRaw = core.getInput('opencode-config').trim()
const opencodeConfig = opencodeConfigRaw.length > 0 ? opencodeConfigRaw : null

// Validate opencode-config is valid JSON if provided
if (opencodeConfig != null) {
validateJsonString(opencodeConfig, 'opencode-config')
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

parseActionInputs() only validates that opencode-config is valid JSON, but action.yaml documents it as a JSON object to merge into config. As written, values like 42, "str", or null will pass validation here but later cannot be merged as an object. Consider validating that the parsed JSON is a plain object (non-null, non-array) to align runtime behavior with the documented input contract.

Suggested change
validateJsonString(opencodeConfig, 'opencode-config')
validateJsonString(opencodeConfig, 'opencode-config')
const parsedOpencodeConfig = JSON.parse(opencodeConfig)
const isObject = typeof parsedOpencodeConfig === 'object'
const isNull = parsedOpencodeConfig === null
const isArray = Array.isArray(parsedOpencodeConfig)
if (!isObject || isNull || isArray) {
throw new Error("Input 'opencode-config' must be a JSON object")
}

Copilot uses AI. Check for mistakes.

const parsedOpencodeConfig: unknown = JSON.parse(opencodeConfig)
const isObject = typeof parsedOpencodeConfig === 'object'
const isNull = parsedOpencodeConfig == null
const isArray = Array.isArray(parsedOpencodeConfig)

if (!isObject || isNull || isArray) {
throw new Error("Input 'opencode-config' must be a JSON object")
}
}

return ok({
githubToken,
authJson,
Expand All @@ -194,6 +216,7 @@ export function parseActionInputs(): Result<ActionInputs, Error> {
skipCache,
omoVersion,
omoProviders,
opencodeConfig,
})
} catch (error) {
return err(error instanceof Error ? error : new Error(String(error)))
Expand Down
Loading
Loading