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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: welan release
name: a welan release

# Fork patch author: only contiguous commits at the tip of origin/welan authored by this
# name are cherry-picked onto the upstream release tag.
Expand Down Expand Up @@ -195,6 +195,11 @@ jobs:
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false"

- name: Run env key priority tests
working-directory: packages/opencode
timeout-minutes: 5
run: bun test --timeout 60000 test/provider/provider-env-key-priority.test.ts

- name: Build CLI binaries
run: OPENCODE_VERSION="$BUILD_VERSION" ./packages/opencode/script/build.ts

Expand Down Expand Up @@ -224,16 +229,10 @@ jobs:
tag="$RELEASE_TAG"
target="$(git rev-parse HEAD)"

if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release delete "$tag" --repo "${{ github.repository }}" --yes
fi

if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then
git push origin ":refs/tags/$tag"
fi
gh release delete "$tag" --repo "${{ github.repository }}" --yes 2>/dev/null || true

git tag -f "$tag" "$target"
git push origin "refs/tags/$tag"
git push origin "refs/tags/$tag" --force

gh release create "$tag" \
/tmp/welan-release-assets/opencode-darwin-arm64 \
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/test-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: welan-test-pr

on:
push:
branches: [welan]
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
checks: write

jobs:
test:
name: unit (linux)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- uses: ./.github/actions/setup-bun

- name: Configure git identity
run: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"

- name: Run opencode tests
working-directory: packages/opencode
timeout-minutes: 20
run: |
find test \( -name '*.test.ts' -o -name '*.test.tsx' \) ! -name 'run-process*.test.ts' -print0 \
| xargs -0 bun test --timeout 60000
bun test --timeout 60000 --max-concurrency=1 test/cli/run/run-process-limit.test.ts
bun test --timeout 60000 --max-concurrency=1 test/cli/run/run-process.test.ts \
--test-name-pattern='exits 0 and writes|prints each|prints reasoning|unknown stream|format json|rejects requested|attach mode|SIGINT'
env:
GITHUB_ACTIONS: "false"
144 changes: 144 additions & 0 deletions docs/superpowers/plans/2026-07-07-env-api-key-priority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Env Var API Key Priority Over Stored Key Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make environment variable API keys (e.g. `OPENCODE_API_KEY`) take precedence over API keys stored via the interactive UI, so users can switch keys at launch time without touching stored configuration.

**Architecture:** In the provider loading sequence inside `provider.ts`, the `// load apikeys` section currently runs after `// load env` and overwrites whatever env var key was loaded. The fix adds a 2-line guard: before merging a stored API key into a provider, check whether that provider's env vars already provided a key. If so, skip the stored key.

**Tech Stack:** TypeScript, Effect, Bun test runner

## Global Constraints

- Run tests from `packages/opencode`, never from repo root
- Use `bun test` (not `bun run test`) to run tests
- Use `bun typecheck` (not `tsc`) for type checking
- Do not move or restructure code blocks — only add the guard inside the existing loop

---

### Task 1: Write the failing test

**Files:**
- Modify: `packages/opencode/test/provider/provider.test.ts` (append at end of file)

**Interfaces:**
- Consumes: `Auth.Service` (already imported as `import { Auth } from "@/auth"`), `set` helper (already defined at line 36), `Global` from `@opencode-ai/core/global`, `Filesystem` from `@/util/filesystem`
- Produces: test case `"env var takes precedence over stored API key"`

- [ ] **Step 1: Append the new test to the test file**

Open `packages/opencode/test/provider/provider.test.ts` and append at the very end:

```typescript
it.instance("env var takes precedence over stored API key", () =>
Effect.gen(function* () {
// Set a stored API key for anthropic
const auth = yield* Auth.Service
yield* auth.set("anthropic", { type: "api", key: "stored-key" })

// Set an env var key — this should win
yield* set("ANTHROPIC_API_KEY", "env-key")

const providers = yield* list
const anthropic = providers[ProviderV2.ID.anthropic]
expect(anthropic).toBeDefined()
// The loaded key should be the env var, not the stored key
expect(anthropic.key).toBe("env-key")
}),
)
```

- [ ] **Step 2: Run the test to verify it fails**

```bash
cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key"
```

Expected: test **FAILS** — currently `anthropic.key` will be `"stored-key"` (stored key wins over env).

---

### Task 2: Implement the fix

**Files:**
- Modify: `packages/opencode/src/provider/provider.ts:1501-1512`

**Interfaces:**
- Consumes: `database` (already in scope — the provider registry with `env` arrays), `envs` (already in scope — loaded env vars map)
- Produces: modified `// load apikeys` loop that skips stored keys when env var is already providing a key

- [ ] **Step 1: Locate the `// load apikeys` loop**

In `packages/opencode/src/provider/provider.ts`, find this block (around line 1501):

```typescript
// load apikeys
const auths = yield* auth.all().pipe(Effect.orDie)
for (const [id, provider] of Object.entries(auths)) {
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
if (provider.type === "api") {
mergeProvider(providerID, {
source: "api",
key: provider.key,
})
}
}
```

- [ ] **Step 2: Add the env-priority guard**

Replace only the inner `if (provider.type === "api")` block:

```typescript
// load apikeys
const auths = yield* auth.all().pipe(Effect.orDie)
for (const [id, provider] of Object.entries(auths)) {
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
if (provider.type === "api") {
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
if (envKey) continue
mergeProvider(providerID, {
source: "api",
key: provider.key,
})
}
}
```

The two new lines are:
1. `const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)` — reuse the same env-var lookup already used in `// load env`
2. `if (envKey) continue` — skip stored key when env var is present

- [ ] **Step 3: Run type check**

```bash
cd packages/opencode && bun typecheck
```

Expected: no errors.

- [ ] **Step 4: Run the new test to verify it passes**

```bash
cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key"
```

Expected: **PASS**

- [ ] **Step 5: Run the full provider test suite to check for regressions**

```bash
cd packages/opencode && bun test test/provider/provider.test.ts
```

Expected: all existing tests **PASS**.

- [ ] **Step 6: Commit**

```bash
git add packages/opencode/src/provider/provider.ts packages/opencode/test/provider/provider.test.ts
git commit -m "fix(core): env var api key takes precedence over stored key"
```
54 changes: 54 additions & 0 deletions docs/superpowers/specs/2026-07-07-env-api-key-priority-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Design: Env Var API Key Takes Precedence Over UI-Stored Key

**Date**: 2026-07-07
**Scope**: `packages/opencode/src/provider/provider.ts`

## Problem

When a user configures an API key through the interactive UI (stored in the auth database), that stored key currently **overrides** any `OPENCODE_API_KEY` environment variable set at launch time. This makes it impossible to switch keys via the command line without modifying stored configuration.

The root cause: in the provider loading sequence, `// load env` runs before `// load apikeys`. Because `mergeDeep` is applied sequentially, the later call (`// load apikeys`) overwrites the `key` field set by the earlier `// load env` call.

## Goal

When a provider's environment variable (e.g. `OPENCODE_API_KEY`) is set at launch time, it must take precedence over any stored API key configured through the UI. This lets users switch keys by setting the env var without touching stored configuration.

## Design

**File**: `packages/opencode/src/provider/provider.ts`
**Location**: `// load apikeys` loop (around line 1502)

In the `// load apikeys` loop, before merging a stored API key into the provider, check whether any of that provider's declared environment variables are already set. If so, skip the stored key — the env var wins.

```typescript
// load apikeys
const auths = yield* auth.all().pipe(Effect.orDie)
for (const [id, provider] of Object.entries(auths)) {
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
if (provider.type === "api") {
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
if (envKey) continue // env var takes precedence over stored key
mergeProvider(providerID, {
source: "api",
key: provider.key,
})
}
}
```

The `database[providerID]?.env` array is the same list of env var names already used in the `// load env` section (e.g. `["OPENCODE_API_KEY"]` for the opencode provider). Re-using this lookup ensures the two sections stay in sync.

## Scope

This change applies to **all providers**, not just opencode. The policy — "an explicitly set runtime env var overrides persisted configuration" — is a sound general principle and reduces special-case branching.

## Non-Goals

- Does not affect the `opencode.ts` plugin's `hasKey` check (line 166), which independently determines whether to operate in public mode. That logic already checks `process.env.OPENCODE_API_KEY` first and remains correct.
- Does not change behavior when no env var is set — stored keys continue to work as before.
- Does not affect OAuth credentials; only `provider.type === "api"` (stored API keys) is touched.

## Merge Conflict Risk

Low. The change is a 2-line addition inside an existing loop. It does not move or restructure any code blocks, minimizing diff size and conflict surface when merging upstream changes.
2 changes: 2 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,8 @@ const layer = Layer.effect(
const providerID = ProviderV2.ID.make(id)
if (disabled.has(providerID)) continue
if (provider.type === "api") {
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
if (envKey) continue
mergeProvider(providerID, {
source: "api",
key: provider.key,
Expand Down
Loading
Loading