Skip to content

Commit bb99ee6

Browse files
QVAC-19908 chore: release opencode-plugin 0.1.0 (#2676)
* QVAC-19908 infra: add @qvac/opencode-plugin release pipeline + target ai-sdk 0.2.2 / cli 0.7 (#2654) * infra: add @qvac/opencode-plugin release pipeline (QVAC-19908) Add the package CI/CD workflow and 0.1.0 changelog artifacts so @qvac/opencode-plugin can build, publish to npm/GPR, and create a GitHub Release from a release-opencode-plugin-* branch. The publish-release-npm job is gated by a verify-release-notes job that runs the same CHANGELOG extraction contract as create-github-release.yml, so a missing or malformed "## [<version>]" section fails before npm publishes rather than after. * chore: target ai-sdk-provider 0.2.2 and cli 0.7 in opencode-plugin (QVAC-19908) Bump @qvac/ai-sdk-provider to ^0.2.2 (whose @qvac/cli peer range is ^0.6.0 || ^0.7.0) and @qvac/cli to ^0.7.0 so the managed host runs the current CLI line and installs cleanly without ERESOLVE. Pin toolsMode: "static" in the managed serve config so the OpenAI-compatible client surface is explicit across CLI versions; the invalid "auto" value otherwise leaves the serve with no loaded model. Update README and changelog requirement references to match. * doc: drop empty Unreleased section from opencode-plugin changelog * fix: repair Qwen tool-call frames in opencode plugin (QVAC-19908) * Revert "fix: repair Qwen tool-call frames in opencode plugin (QVAC-19908)" This reverts commit 0dff076.
1 parent f05e794 commit bb99ee6

7 files changed

Lines changed: 448 additions & 6 deletions

File tree

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
name: General CI/CD (opencode-plugin)
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- release-*
8+
- feature-*
9+
- tmp-*
10+
paths:
11+
- "plugins/opencode/**"
12+
workflow_dispatch:
13+
inputs:
14+
npm_tag:
15+
description: "Optional npm dist-tag override for release branches"
16+
required: false
17+
type: string
18+
19+
permissions:
20+
contents: read
21+
22+
env:
23+
WORKDIR: plugins/opencode
24+
25+
jobs:
26+
label-gate:
27+
name: Authorise (label-gate)
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: read
31+
pull-requests: write
32+
outputs:
33+
authorised: ${{ steps.gate.outputs.authorised }}
34+
steps:
35+
- name: Checkout (label-gate action only)
36+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
37+
with:
38+
ref: ${{ github.event.repository.default_branch }}
39+
sparse-checkout: .github/actions/label-gate
40+
sparse-checkout-cone-mode: false
41+
42+
- name: Run label-gate
43+
id: gate
44+
uses: ./.github/actions/label-gate
45+
with:
46+
github-token: ${{ secrets.PAT_TOKEN }}
47+
48+
release-merge-guard:
49+
name: Release Merge Guard
50+
if: >-
51+
github.event_name == 'push' && startsWith(github.ref_name, 'release-')
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
55+
with:
56+
fetch-depth: 0
57+
58+
- uses: ./.github/actions/release-merge-guard
59+
with:
60+
github-token: ${{ secrets.GITHUB_TOKEN }}
61+
base-ref: ${{ github.ref_name }}
62+
base-sha: ${{ github.event.before }}
63+
head-sha: ${{ github.sha }}
64+
package-slug: opencode-plugin
65+
package-json-path: plugins/opencode/package.json
66+
changelog-path: plugins/opencode/CHANGELOG.md
67+
68+
verify-release-notes:
69+
name: Verify release notes
70+
if: >-
71+
github.event_name == 'push' && startsWith(github.ref_name, 'release-')
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
75+
76+
- name: Verify CHANGELOG section for release version is present and non-empty
77+
working-directory: ${{ env.WORKDIR }}
78+
run: |
79+
set -euo pipefail
80+
version="$(node -p "require('./package.json').version")"
81+
changelog_path="CHANGELOG.md"
82+
83+
if [ ! -s "${changelog_path}" ]; then
84+
echo "::error::Missing or empty changelog: ${WORKDIR}/${changelog_path}"
85+
exit 1
86+
fi
87+
88+
start_line=$(awk -v version="${version}" '$0 ~ "^## \\[" version "\\]" { print NR; exit }' "${changelog_path}")
89+
if [ -z "${start_line}" ]; then
90+
echo "::error::Could not find changelog heading '## [${version}]' in ${WORKDIR}/${changelog_path}"
91+
exit 1
92+
fi
93+
94+
end_line=$(awk -v start="${start_line}" 'NR > start && /^## \[/ { print NR - 1; found=1; exit } END { if (!found) print NR }' "${changelog_path}")
95+
if [ -z "${end_line}" ] || [ "${end_line}" -lt "${start_line}" ]; then
96+
echo "::error::Invalid changelog section bounds for version ${version}"
97+
exit 1
98+
fi
99+
100+
if ! sed -n "$((start_line + 1)),$((end_line))p" "${changelog_path}" | awk 'BEGIN { has_content=0 } /[^[:space:]]/ { has_content=1 } END { exit has_content ? 0 : 1 }'; then
101+
echo "::error::Changelog section for version ${version} is empty; GitHub Release creation would fail after npm publish"
102+
exit 1
103+
fi
104+
105+
echo "Release notes for ${version} found and non-empty."
106+
107+
publish-logic:
108+
runs-on: ubuntu-latest
109+
outputs:
110+
publish_main: ${{ steps.logic.outputs.publish_main }}
111+
publish_release: ${{ steps.logic.outputs.publish_release }}
112+
publish_feature: ${{ steps.logic.outputs.publish_feature }}
113+
publish_tmp: ${{ steps.logic.outputs.publish_tmp }}
114+
gpr_tag: ${{ steps.logic.outputs.gpr_tag }}
115+
npm_tag: ${{ steps.logic.outputs.npm_tag }}
116+
steps:
117+
- id: logic
118+
shell: bash
119+
env:
120+
NPM_TAG_INPUT: ${{ inputs.npm_tag }}
121+
run: |
122+
set -euo pipefail
123+
ref_name="${GITHUB_REF_NAME}"
124+
event_name="${GITHUB_EVENT_NAME}"
125+
126+
publish_main="false"
127+
publish_release="false"
128+
publish_feature="false"
129+
publish_tmp="false"
130+
131+
if [ "$event_name" = "push" ] || [ "$event_name" = "workflow_dispatch" ]; then
132+
if [ "$ref_name" = "main" ]; then
133+
publish_main="true"
134+
elif [[ "$ref_name" == release-* ]]; then
135+
publish_release="true"
136+
elif [[ "$ref_name" == feature-* ]]; then
137+
publish_feature="true"
138+
elif [[ "$ref_name" == tmp-* ]]; then
139+
publish_tmp="true"
140+
fi
141+
fi
142+
143+
gpr_tag="dev"
144+
if [ "$ref_name" = "main" ]; then
145+
gpr_tag="dev"
146+
elif [[ "$ref_name" == feature-* ]]; then
147+
gpr_tag="feature"
148+
elif [[ "$ref_name" == tmp-* ]]; then
149+
gpr_tag="temp"
150+
fi
151+
152+
echo "publish_main=$publish_main" >> "$GITHUB_OUTPUT"
153+
echo "publish_release=$publish_release" >> "$GITHUB_OUTPUT"
154+
echo "publish_feature=$publish_feature" >> "$GITHUB_OUTPUT"
155+
echo "publish_tmp=$publish_tmp" >> "$GITHUB_OUTPUT"
156+
echo "gpr_tag=$gpr_tag" >> "$GITHUB_OUTPUT"
157+
echo "npm_tag=${NPM_TAG_INPUT}" >> "$GITHUB_OUTPUT"
158+
159+
build:
160+
name: Build package
161+
runs-on: ubuntu-latest
162+
steps:
163+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
164+
165+
- name: Setup Node
166+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # 6.3.0
167+
with:
168+
node-version: 22
169+
170+
- name: Install dependencies
171+
working-directory: ${{ env.WORKDIR }}
172+
run: npm install
173+
174+
- name: Run lint
175+
working-directory: ${{ env.WORKDIR }}
176+
run: npm run lint
177+
178+
- name: Run tests
179+
working-directory: ${{ env.WORKDIR }}
180+
run: npm run test:unit
181+
182+
- name: Build package
183+
working-directory: ${{ env.WORKDIR }}
184+
run: npm run build
185+
186+
- name: Upload package dist
187+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
188+
with:
189+
name: opencode-plugin-dist
190+
path: plugins/opencode/dist
191+
if-no-files-found: error
192+
193+
publish-main-gpr-dev:
194+
needs:
195+
- publish-logic
196+
- label-gate
197+
- build
198+
if: needs.label-gate.outputs.authorised == 'true' && needs.publish-logic.outputs.publish_main == 'true'
199+
runs-on: ubuntu-latest
200+
environment: release
201+
permissions:
202+
contents: read
203+
packages: write
204+
steps:
205+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
206+
207+
- name: Download package dist
208+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
209+
with:
210+
name: opencode-plugin-dist
211+
path: plugins/opencode/dist
212+
213+
- name: Publish to GitHub Packages (dev)
214+
uses: ./.github/actions/publish-library-to-gpr
215+
with:
216+
secret-token: ${{ secrets.GITHUB_TOKEN }}
217+
npm-token: ${{ secrets.NPM_TOKEN }}
218+
tag: ${{ needs.publish-logic.outputs.gpr_tag }}
219+
workdir: plugins/opencode
220+
name-suffix: "-mono"
221+
222+
publish-release-npm:
223+
needs:
224+
- publish-logic
225+
- release-merge-guard
226+
- verify-release-notes
227+
- label-gate
228+
- build
229+
if: |-
230+
needs.label-gate.outputs.authorised == 'true' && (always() && needs.publish-logic.outputs.publish_release == 'true' && (needs.release-merge-guard.result == 'success' || needs.release-merge-guard.result == 'skipped') && (needs.verify-release-notes.result == 'success' || needs.verify-release-notes.result == 'skipped'))
231+
runs-on: ubuntu-latest
232+
environment: npm
233+
outputs:
234+
published_version: ${{ steps.publish-npm.outputs.npm_published_version }}
235+
permissions:
236+
contents: write
237+
packages: write
238+
id-token: write
239+
steps:
240+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
241+
with:
242+
fetch-depth: 0
243+
244+
- name: Download package dist
245+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
246+
with:
247+
name: opencode-plugin-dist
248+
path: plugins/opencode/dist
249+
250+
- name: Publish to NPM
251+
id: publish-npm
252+
uses: ./.github/actions/publish-library-to-npm
253+
with:
254+
tag: ${{ needs.publish-logic.outputs.npm_tag }}
255+
workdir: plugins/opencode
256+
257+
publish-release:
258+
needs: [publish-release-npm]
259+
if: needs.publish-release-npm.result == 'success' && needs.publish-release-npm.outputs.published_version != ''
260+
permissions:
261+
contents: write
262+
uses: ./.github/workflows/create-github-release.yml
263+
with:
264+
repo_name: "opencode-plugin"
265+
release_name: "QVAC OpenCode Plugin"
266+
published_version: ${{ needs.publish-release-npm.outputs.published_version }}
267+
prev_sha: ${{ github.event.before }}
268+
workdir: "plugins/opencode"
269+
270+
publish-feature-gpr:
271+
needs:
272+
- publish-logic
273+
- label-gate
274+
- build
275+
if: needs.label-gate.outputs.authorised == 'true' && needs.publish-logic.outputs.publish_feature == 'true'
276+
runs-on: ubuntu-latest
277+
environment: release
278+
permissions:
279+
contents: read
280+
packages: write
281+
steps:
282+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
283+
284+
- name: Download package dist
285+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
286+
with:
287+
name: opencode-plugin-dist
288+
path: plugins/opencode/dist
289+
290+
- name: Publish to GitHub Packages (feature)
291+
uses: ./.github/actions/publish-library-to-gpr
292+
with:
293+
secret-token: ${{ secrets.GITHUB_TOKEN }}
294+
npm-token: ${{ secrets.NPM_TOKEN }}
295+
tag: ${{ needs.publish-logic.outputs.gpr_tag }}
296+
workdir: plugins/opencode
297+
name-suffix: "-mono"
298+
299+
publish-tmp-gpr:
300+
needs:
301+
- publish-logic
302+
- label-gate
303+
- build
304+
if: needs.label-gate.outputs.authorised == 'true' && needs.publish-logic.outputs.publish_tmp == 'true'
305+
runs-on: ubuntu-latest
306+
environment: release
307+
permissions:
308+
contents: read
309+
packages: write
310+
steps:
311+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
312+
313+
- name: Download package dist
314+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
315+
with:
316+
name: opencode-plugin-dist
317+
path: plugins/opencode/dist
318+
319+
- name: Publish to GitHub Packages (tmp)
320+
uses: ./.github/actions/publish-library-to-gpr
321+
with:
322+
secret-token: ${{ secrets.GITHUB_TOKEN }}
323+
npm-token: ${{ secrets.NPM_TOKEN }}
324+
tag: ${{ needs.publish-logic.outputs.gpr_tag }}
325+
workdir: plugins/opencode
326+
name-suffix: "-mono"

plugins/opencode/CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Changelog
2+
3+
## [0.1.0]
4+
5+
Release Date: 2026-06-16
6+
7+
📦 **NPM:** https://www.npmjs.com/package/@qvac/opencode-plugin/v/0.1.0
8+
9+
The first public release of `@qvac/opencode-plugin` — a turnkey [OpenCode](https://opencode.ai) plugin that runs a local, fully managed QVAC serve so `opencode` works against on-device models with no second terminal and no manual server.
10+
11+
### Added
12+
13+
- **Zero-config local OpenCode.** Adding `@qvac/opencode-plugin` to a project's `opencode.json` `plugin` array is enough: on startup the plugin brings up a managed `qvac serve`, injects an OpenAI-compatible `qvac` provider pointed at it, sets it as the project default model, and tears the serve down on exit. No `provider` block, no second terminal, no `QVAC_MODEL=` prefix.
14+
- **Managed serve host process.** The plugin spawns a host child process in a real Node/Bun runtime (OpenCode runs plugins inside its own compiled binary, which cannot spawn the detached managed-mode supervisor). The host runs `createQvac({ mode: 'managed' })` from [`@qvac/ai-sdk-provider`](https://www.npmjs.com/package/@qvac/ai-sdk-provider), which brings up a shared, idle-reaped serve on an auto-allocated port, and ensures the serve is reaped even if OpenCode is killed hard.
15+
- **Non-blocking startup.** The host starts a small local proxy and reports it is listening before the model finishes downloading, so `opencode run` never trips OpenCode's startup timeout; the model loads in the background and the first turn waits on it.
16+
- **Shared serve across windows.** Multiple OpenCode windows share one serve (the provider's `reuse` default); the detached runner owns the loaded model and reaps it a few minutes after the last session leaves, so a second window doesn't reload the model.
17+
- **Friendly model ids.** A models.dev-style id (e.g. `qwen3.5-9b`) flows through OpenCode's model picker and the request `model` field, with the friendly-id → QVAC constant mapping resolved via `@qvac/ai-sdk-provider`'s catalog. Defaults to `qwen3.5-9b`.
18+
- **Layered configuration.** Options resolve from built-in defaults, a project `qvac.json`, the `opencode.json` plugin-tuple options, and `QVAC_*` environment variables (in increasing precedence): `model`, `ctxSize`, `reasoningBudget`, `tools`, `shim`, `runtime`, `readyTimeoutMs`, `setDefaultModel`, and `debug`.
19+
- **OpenAI-compatibility shim.** An in-process proxy bridges `@ai-sdk/openai-compatible` and QVAC serve: it flattens array `content` to the string form serve currently accepts, and re-routes inline `<think>…</think>` reasoning to `reasoning_content` so OpenCode renders a collapsed "Thought" block. Disable with `shim: false` / `QVAC_SHIM=0` once serve closes those gaps; the proxy itself remains (it is what lets startup return before the model loads).
20+
- **Examples.** Minimal and fully-annotated `opencode.json` examples for adding the plugin with and without options.
21+
- **Explicit static tools mode.** The managed serve config pins `toolsMode: "static"` so the OpenAI-compatible client surface is unambiguous across CLI versions (the invalid `"auto"` value leaves the serve with no loaded model).
22+
23+
### Requirements
24+
25+
- [`@qvac/ai-sdk-provider@^0.2.2`](https://www.npmjs.com/package/@qvac/ai-sdk-provider) for managed mode.
26+
- [`@qvac/cli@^0.7.0`](https://www.npmjs.com/package/@qvac/cli) so the host can run `qvac serve` (resolved by the provider's managed mode).

plugins/opencode/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ running `opencode` from the terminal.
117117

118118
## Requirements
119119

120-
- [`@qvac/ai-sdk-provider@^0.2.1`](https://www.npmjs.com/package/@qvac/ai-sdk-provider)
120+
- [`@qvac/ai-sdk-provider@^0.2.2`](https://www.npmjs.com/package/@qvac/ai-sdk-provider)
121121
for managed mode.
122-
- [`@qvac/cli`](https://www.npmjs.com/package/@qvac/cli) available so the host
123-
can run `qvac serve` (resolved by the provider's managed mode).
122+
- [`@qvac/cli@^0.7.0`](https://www.npmjs.com/package/@qvac/cli) available so the
123+
host can run `qvac serve` (resolved by the provider's managed mode).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog v0.1.0
2+
3+
Release Date: 2026-06-16
4+
5+
## ✨ Features
6+
7+
- Add `@qvac/opencode-plugin`: a turnkey OpenCode plugin that runs a local, managed `qvac serve` so `opencode` works against on-device models with no second terminal. (see PR [#2521](https://github.com/tetherto/qvac/pull/2521))

0 commit comments

Comments
 (0)