Skip to content

Commit c149f7f

Browse files
committed
env OPENCODE_API_KEY prioritizes over the stored api key
Signed-off-by: weizhoublue <weizhou.lan@daocloud.io>
1 parent 5081595 commit c149f7f

7 files changed

Lines changed: 419 additions & 37 deletions

File tree

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: welan release
1+
name: a welan release
22

33
# Fork patch author: only contiguous commits at the tip of origin/welan authored by this
44
# name are cherry-picked onto the upstream release tag.
@@ -20,7 +20,7 @@ on:
2020
type: string
2121

2222
concurrency:
23-
group: welan-release
23+
group: ${{ github.workflow }}
2424
cancel-in-progress: false
2525

2626
permissions:
@@ -30,6 +30,7 @@ env:
3030
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
3131
GH_TOKEN: ${{ secrets.WELAN_PAT }}
3232
WELAN_PATCH_AUTHOR: weizhoublue
33+
BRANCH_NAME: welan
3334

3435
jobs:
3536
release:
@@ -77,43 +78,54 @@ jobs:
7778
7879
- name: Fetch branches and tags
7980
run: |
80-
git fetch origin welan:refs/remotes/origin/welan --tags
81+
git fetch origin "$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME" --tags
8182
git fetch upstream "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
8283
git rev-parse --verify "refs/tags/$RELEASE_TAG^{commit}" >/dev/null
8384
84-
- name: Prepare welan from upstream tag
85+
- name: Prepare branch from upstream tag
8586
run: |
86-
git checkout -B welan "$RELEASE_TAG"
87+
git checkout -B "$BRANCH_NAME" "$RELEASE_TAG"
8788
88-
- name: Cherry-pick welan author patches
89+
- name: Cherry-pick author patches
8990
id: patches
9091
continue-on-error: true
9192
run: |
92-
patch_commits=()
93-
ref="origin/welan"
94-
93+
# Walk the first-parent chain to find the upstream boundary: the first
94+
# commit NOT authored by WELAN_PATCH_AUTHOR. Merge commits count as
95+
# authored by whoever merged them, so this correctly stops at the
96+
# upstream base even when the tip is a merge commit.
97+
base_ref="origin/$BRANCH_NAME"
9598
while true; do
96-
author="$(git log -1 --format='%an' "$ref")"
99+
author="$(git log -1 --format='%an' "$base_ref")"
97100
if [[ "$author" != *"$WELAN_PATCH_AUTHOR"* ]]; then
98101
break
99102
fi
100-
101-
patch_commits=("$ref" "${patch_commits[@]}")
102-
103-
if ! git rev-parse "${ref}^" >/dev/null 2>&1; then
103+
if ! git rev-parse "${base_ref}^" >/dev/null 2>&1; then
104104
break
105105
fi
106-
ref="${ref}^"
106+
base_ref="${base_ref}^"
107107
done
108108
109+
if [[ "$(git rev-parse "$base_ref")" == "$(git rev-parse "origin/$BRANCH_NAME")" ]]; then
110+
echo "::error::No commits authored by $WELAN_PATCH_AUTHOR found at the tip of origin/$BRANCH_NAME"
111+
exit 1
112+
fi
113+
114+
# Collect all non-merge commits in the weizhoublue range (oldest first).
115+
# --no-merges skips merge bookkeeping commits and instead surfaces the
116+
# real patch commits from any merged branches, making each entry safely
117+
# cherry-pickable without needing -m.
118+
mapfile -t patch_commits < <(
119+
git log --no-merges --reverse --format='%H' "${base_ref}..origin/$BRANCH_NAME"
120+
)
121+
109122
if [ "${#patch_commits[@]}" -eq 0 ]; then
110-
echo "::error::No commits authored by $WELAN_PATCH_AUTHOR found at the tip of origin/welan"
123+
echo "::error::No non-merge commits found between $base_ref and origin/$BRANCH_NAME"
111124
exit 1
112125
fi
113126
114-
echo "Cherry-picking ${#patch_commits[@]} commit(s) from origin/welan (author: $WELAN_PATCH_AUTHOR)"
115-
for commit in "${patch_commits[@]}"; do
116-
sha="$(git rev-parse "$commit")"
127+
echo "Cherry-picking ${#patch_commits[@]} commit(s) from origin/$BRANCH_NAME (author: $WELAN_PATCH_AUTHOR)"
128+
for sha in "${patch_commits[@]}"; do
117129
subject="$(git log -1 --format='%s' "$sha")"
118130
echo "Cherry-picking $sha $subject"
119131
git cherry-pick "$sha"
@@ -124,15 +136,15 @@ jobs:
124136
run: |
125137
conflicted_files="$(git diff --name-only --diff-filter=U || true)"
126138
upstream_tag="$(git rev-parse "$RELEASE_TAG")"
127-
origin_welan="$(git rev-parse origin/welan)"
139+
origin_branch="$(git rev-parse "origin/$BRANCH_NAME")"
128140
current_head="$(git rev-parse HEAD)"
129141
130-
echo "::error::welan release $RELEASE_TAG hit a cherry-pick conflict"
142+
echo "::error::$BRANCH_NAME release $RELEASE_TAG hit a cherry-pick conflict"
131143
echo "Version: $RELEASE_TAG"
132144
echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
133145
echo "Upstream tag: $RELEASE_TAG"
134146
echo "Upstream tag SHA: ${upstream_tag}"
135-
echo "origin/welan: ${origin_welan}"
147+
echo "origin/$BRANCH_NAME: ${origin_branch}"
136148
echo "Current HEAD during conflict: ${current_head}"
137149
echo "Patch author: $WELAN_PATCH_AUTHOR"
138150
echo
@@ -195,12 +207,17 @@ jobs:
195207
env:
196208
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false"
197209

210+
- name: Run env key priority tests
211+
working-directory: packages/opencode
212+
timeout-minutes: 5
213+
run: bun test --timeout 60000 test/provider/provider-env-key-priority.test.ts
214+
198215
- name: Build CLI binaries
199216
run: OPENCODE_VERSION="$BUILD_VERSION" ./packages/opencode/script/build.ts
200217

201218
- name: Stage release binaries
202219
run: |
203-
asset_dir="/tmp/welan-release-assets"
220+
asset_dir="/tmp/$BRANCH_NAME-release-assets"
204221
rm -rf "$asset_dir"
205222
mkdir -p "$asset_dir"
206223
@@ -210,8 +227,8 @@ jobs:
210227
211228
ls -lh "$asset_dir"
212229
213-
- name: Push rebased welan
214-
run: git push origin HEAD:welan --force-with-lease
230+
- name: Push rebased branch
231+
run: git push origin "HEAD:$BRANCH_NAME" --force-with-lease
215232

216233
- name: Push release archive branch
217234
run: |
@@ -224,21 +241,15 @@ jobs:
224241
tag="$RELEASE_TAG"
225242
target="$(git rev-parse HEAD)"
226243
227-
if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then
228-
gh release delete "$tag" --repo "${{ github.repository }}" --yes
229-
fi
230-
231-
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then
232-
git push origin ":refs/tags/$tag"
233-
fi
244+
gh release delete "$tag" --repo "${{ github.repository }}" --yes 2>/dev/null || true
234245
235246
git tag -f "$tag" "$target"
236-
git push origin "refs/tags/$tag"
247+
git push origin "refs/tags/$tag" --force
237248
238249
gh release create "$tag" \
239-
/tmp/welan-release-assets/opencode-darwin-arm64 \
240-
/tmp/welan-release-assets/opencode-linux-arm64 \
241-
/tmp/welan-release-assets/opencode-linux-x64 \
250+
"$asset_dir/opencode-darwin-arm64" \
251+
"$asset_dir/opencode-linux-arm64" \
252+
"$asset_dir/opencode-linux-x64" \
242253
--repo "${{ github.repository }}" \
243254
--target "$target" \
244255
--title "$tag" \

.github/workflows/test-linux.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: welan-test-pr
2+
3+
on:
4+
push:
5+
branches: [welan]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
checks: write
16+
17+
jobs:
18+
test:
19+
name: unit (linux)
20+
runs-on: ubuntu-24.04
21+
steps:
22+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
23+
24+
- uses: ./.github/actions/setup-bun
25+
26+
- name: Configure git identity
27+
run: |
28+
git config --global user.email "bot@opencode.ai"
29+
git config --global user.name "opencode"
30+
31+
- name: Run opencode tests
32+
working-directory: packages/opencode
33+
timeout-minutes: 20
34+
run: |
35+
find test \( -name '*.test.ts' -o -name '*.test.tsx' \) ! -name 'run-process*.test.ts' -print0 \
36+
| xargs -0 bun test --timeout 60000
37+
bun test --timeout 60000 --max-concurrency=1 test/cli/run/run-process-limit.test.ts
38+
bun test --timeout 60000 --max-concurrency=1 test/cli/run/run-process.test.ts \
39+
--test-name-pattern='exits 0 and writes|prints each|prints reasoning|unknown stream|format json|rejects requested|attach mode|SIGINT'
40+
env:
41+
GITHUB_ACTIONS: "false"
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Env Var API Key Priority Over Stored Key Implementation Plan
2+
3+
> **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.
4+
5+
**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.
6+
7+
**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.
8+
9+
**Tech Stack:** TypeScript, Effect, Bun test runner
10+
11+
## Global Constraints
12+
13+
- Run tests from `packages/opencode`, never from repo root
14+
- Use `bun test` (not `bun run test`) to run tests
15+
- Use `bun typecheck` (not `tsc`) for type checking
16+
- Do not move or restructure code blocks — only add the guard inside the existing loop
17+
18+
---
19+
20+
### Task 1: Write the failing test
21+
22+
**Files:**
23+
- Modify: `packages/opencode/test/provider/provider.test.ts` (append at end of file)
24+
25+
**Interfaces:**
26+
- 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`
27+
- Produces: test case `"env var takes precedence over stored API key"`
28+
29+
- [ ] **Step 1: Append the new test to the test file**
30+
31+
Open `packages/opencode/test/provider/provider.test.ts` and append at the very end:
32+
33+
```typescript
34+
it.instance("env var takes precedence over stored API key", () =>
35+
Effect.gen(function* () {
36+
// Set a stored API key for anthropic
37+
const auth = yield* Auth.Service
38+
yield* auth.set("anthropic", { type: "api", key: "stored-key" })
39+
40+
// Set an env var key — this should win
41+
yield* set("ANTHROPIC_API_KEY", "env-key")
42+
43+
const providers = yield* list
44+
const anthropic = providers[ProviderV2.ID.anthropic]
45+
expect(anthropic).toBeDefined()
46+
// The loaded key should be the env var, not the stored key
47+
expect(anthropic.key).toBe("env-key")
48+
}),
49+
)
50+
```
51+
52+
- [ ] **Step 2: Run the test to verify it fails**
53+
54+
```bash
55+
cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key"
56+
```
57+
58+
Expected: test **FAILS** — currently `anthropic.key` will be `"stored-key"` (stored key wins over env).
59+
60+
---
61+
62+
### Task 2: Implement the fix
63+
64+
**Files:**
65+
- Modify: `packages/opencode/src/provider/provider.ts:1501-1512`
66+
67+
**Interfaces:**
68+
- Consumes: `database` (already in scope — the provider registry with `env` arrays), `envs` (already in scope — loaded env vars map)
69+
- Produces: modified `// load apikeys` loop that skips stored keys when env var is already providing a key
70+
71+
- [ ] **Step 1: Locate the `// load apikeys` loop**
72+
73+
In `packages/opencode/src/provider/provider.ts`, find this block (around line 1501):
74+
75+
```typescript
76+
// load apikeys
77+
const auths = yield* auth.all().pipe(Effect.orDie)
78+
for (const [id, provider] of Object.entries(auths)) {
79+
const providerID = ProviderV2.ID.make(id)
80+
if (disabled.has(providerID)) continue
81+
if (provider.type === "api") {
82+
mergeProvider(providerID, {
83+
source: "api",
84+
key: provider.key,
85+
})
86+
}
87+
}
88+
```
89+
90+
- [ ] **Step 2: Add the env-priority guard**
91+
92+
Replace only the inner `if (provider.type === "api")` block:
93+
94+
```typescript
95+
// load apikeys
96+
const auths = yield* auth.all().pipe(Effect.orDie)
97+
for (const [id, provider] of Object.entries(auths)) {
98+
const providerID = ProviderV2.ID.make(id)
99+
if (disabled.has(providerID)) continue
100+
if (provider.type === "api") {
101+
const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)
102+
if (envKey) continue
103+
mergeProvider(providerID, {
104+
source: "api",
105+
key: provider.key,
106+
})
107+
}
108+
}
109+
```
110+
111+
The two new lines are:
112+
1. `const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)` — reuse the same env-var lookup already used in `// load env`
113+
2. `if (envKey) continue` — skip stored key when env var is present
114+
115+
- [ ] **Step 3: Run type check**
116+
117+
```bash
118+
cd packages/opencode && bun typecheck
119+
```
120+
121+
Expected: no errors.
122+
123+
- [ ] **Step 4: Run the new test to verify it passes**
124+
125+
```bash
126+
cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key"
127+
```
128+
129+
Expected: **PASS**
130+
131+
- [ ] **Step 5: Run the full provider test suite to check for regressions**
132+
133+
```bash
134+
cd packages/opencode && bun test test/provider/provider.test.ts
135+
```
136+
137+
Expected: all existing tests **PASS**.
138+
139+
- [ ] **Step 6: Commit**
140+
141+
```bash
142+
git add packages/opencode/src/provider/provider.ts packages/opencode/test/provider/provider.test.ts
143+
git commit -m "fix(core): env var api key takes precedence over stored key"
144+
```

0 commit comments

Comments
 (0)