Skip to content

Commit 8f40e61

Browse files
feat(ci): add release workflow and clean up plugin packaging
- Add .github/workflows/release.yml — publishes to npm on v* tag push - Remove root index.ts re-export; point package.json main/module at src/index.ts - Add files field to package.json to limit published content to src/ - Add version 0.0.1 to package.json - Rename OPENCODE_OTLP_HEADERS and OPENCODE_RESOURCE_ATTRIBUTES from bare OTEL_* names; loadConfig copies them to the standard OTEL_* vars before SDK init - Remove parseHeaders from otel.ts (SDK now reads OTEL_EXPORTER_OTLP_HEADERS natively) - Update tests and docs to reflect all changes
1 parent 63ca65a commit 8f40e61

10 files changed

Lines changed: 93 additions & 74 deletions

File tree

.github/workflows/release.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
release:
10+
name: Publish to npm
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: "22"
19+
registry-url: "https://registry.npmjs.org"
20+
21+
- uses: oven-sh/setup-bun@v2
22+
with:
23+
bun-version: latest
24+
25+
- name: Install dependencies
26+
run: bun install --frozen-lockfile
27+
28+
- name: Typecheck
29+
run: bun run typecheck
30+
31+
- name: Test
32+
run: bun test
33+
34+
- name: Publish to npm
35+
run: npm publish --access public
36+
env:
37+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

AGENTS.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ Always run after making changes:
1010
bun run typecheck
1111
```
1212

13-
There is no separate build step needed for local development. For publishing:
14-
15-
```bash
16-
bun run build
17-
```
13+
There is no build step. TypeScript source files are published directly and loaded natively by Bun.
1814

1915
## Testing
2016

@@ -48,7 +44,7 @@ src/
4844
- **`setBoundedMap`** — always use this instead of `Map.set` for `pendingToolSpans` and `pendingPermissions` to prevent unbounded growth.
4945
- **Single source of truth for tokens/cost** — token and cost counters are incremented only in `message.updated` (`src/handlers/message.ts`), never in `step-finish`.
5046
- **Shutdown** — OTel providers are flushed via `SIGTERM`/`SIGINT`/`beforeExit`. Do not use `process.on("exit")` for async flushing.
51-
- **All env vars are `OPENCODE_` prefixed**`OPENCODE_ENABLE_TELEMETRY`, `OPENCODE_OTLP_ENDPOINT`, `OPENCODE_OTLP_METRICS_INTERVAL`, `OPENCODE_OTLP_LOGS_INTERVAL`, `OPENCODE_METRIC_PREFIX`. Never use bare `OTEL_*` names for plugin config.
47+
- **All env vars are `OPENCODE_` prefixed**`OPENCODE_ENABLE_TELEMETRY`, `OPENCODE_OTLP_ENDPOINT`, `OPENCODE_OTLP_METRICS_INTERVAL`, `OPENCODE_OTLP_LOGS_INTERVAL`, `OPENCODE_METRIC_PREFIX`, `OPENCODE_OTLP_HEADERS`, `OPENCODE_RESOURCE_ATTRIBUTES`. Never use bare `OTEL_*` names for plugin config. `loadConfig` copies `OPENCODE_OTLP_HEADERS``OTEL_EXPORTER_OTLP_HEADERS` and `OPENCODE_RESOURCE_ATTRIBUTES``OTEL_RESOURCE_ATTRIBUTES` before the SDK initializes.
5248
- **`OPENCODE_ENABLE_TELEMETRY`** — all OTel instrumentation is gated on this env var. The plugin always loads regardless; only telemetry is disabled when unset.
5349
- **`OPENCODE_METRIC_PREFIX`** — defaults to `opencode.`; set to `claude_code.` for Claude Code dashboard compatibility.
5450

CONTRIBUTING.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Point your local opencode config at the repo so changes are picked up immediatel
2020
```json
2121
{
2222
"$schema": "https://opencode.ai/config.json",
23-
"plugin": ["/path/to/opencode-plugin-otel/index.ts"]
23+
"plugin": ["/path/to/opencode-plugin-otel/src/index.ts"]
2424
}
2525
```
2626

@@ -32,7 +32,6 @@ opencode loads TypeScript natively via Bun, so there is no build step required d
3232
|---------|-------------|
3333
| `bun run typecheck` | Type-check all sources without emitting |
3434
| `bun test` | Run the test suite |
35-
| `bun run build` | Compile to `dist/` for publishing |
3635

3736
## Project structure
3837

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Or point directly at a local checkout for development:
4545
```json
4646
{
4747
"$schema": "https://opencode.ai/config.json",
48-
"plugin": ["/path/to/opencode-plugin-otel/index.ts"]
48+
"plugin": ["/path/to/opencode-plugin-otel/src/index.ts"]
4949
}
5050
```
5151

@@ -60,6 +60,8 @@ All configuration is via environment variables. Set them in your shell profile (
6060
| `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds |
6161
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
6262
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
63+
| `OPENCODE_OTLP_HEADERS` | _(unset)_ | Comma-separated `key=value` headers added to all OTLP exports (e.g. for auth tokens) |
64+
| `OPENCODE_RESOURCE_ATTRIBUTES` | _(unset)_ | Comma-separated `key=value` pairs merged into the OTel resource |
6365

6466
### Quick start
6567

index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
{
22
"name": "opencode-plugin-otel",
3-
"module": "index.ts",
3+
"version": "0.0.1",
4+
"module": "src/index.ts",
5+
"main": "src/index.ts",
46
"type": "module",
7+
"files": [
8+
"src/"
9+
],
510
"devDependencies": {
611
"@types/bun": "latest"
712
},
813
"scripts": {
9-
"release:patch": "npm version patch && npm publish",
10-
"release:minor": "npm version minor && npm publish",
11-
"release:major": "npm version major && npm publish",
1214
"typecheck": "tsc --noEmit"
1315
},
1416
"dependencies": {

src/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type PluginConfig = {
66
metricsInterval: number
77
logsInterval: number
88
metricPrefix: string
9+
otlpHeaders: string | undefined
10+
resourceAttributes: string | undefined
911
}
1012

1113
export function parseEnvInt(key: string, fallback: number): number {
@@ -16,12 +18,20 @@ export function parseEnvInt(key: string, fallback: number): number {
1618
}
1719

1820
export function loadConfig(): PluginConfig {
21+
const otlpHeaders = process.env["OPENCODE_OTLP_HEADERS"]
22+
const resourceAttributes = process.env["OPENCODE_RESOURCE_ATTRIBUTES"]
23+
24+
if (otlpHeaders) process.env["OTEL_EXPORTER_OTLP_HEADERS"] = otlpHeaders
25+
if (resourceAttributes) process.env["OTEL_RESOURCE_ATTRIBUTES"] = resourceAttributes
26+
1927
return {
2028
enabled: !!process.env["OPENCODE_ENABLE_TELEMETRY"],
2129
endpoint: process.env["OPENCODE_OTLP_ENDPOINT"] ?? "http://localhost:4317",
2230
metricsInterval: parseEnvInt("OPENCODE_OTLP_METRICS_INTERVAL", 60000),
2331
logsInterval: parseEnvInt("OPENCODE_OTLP_LOGS_INTERVAL", 5000),
2432
metricPrefix: process.env["OPENCODE_METRIC_PREFIX"] ?? "opencode.",
33+
otlpHeaders,
34+
resourceAttributes,
2535
}
2636
}
2737

src/otel.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,6 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"
99
import { ATTR_HOST_ARCH } from "@opentelemetry/semantic-conventions/incubating"
1010
import type { Instruments } from "./types.ts"
1111

12-
export function parseHeaders(raw: string | undefined): Record<string, string> {
13-
if (!raw) return {}
14-
const result: Record<string, string> = {}
15-
for (const pair of raw.split(",")) {
16-
const idx = pair.indexOf("=")
17-
if (idx > 0) {
18-
const key = pair.slice(0, idx).trim()
19-
const val = pair.slice(idx + 1).trim()
20-
if (key) result[key] = val
21-
}
22-
}
23-
return result
24-
}
25-
2612
export function buildResource(version: string) {
2713
const attrs: Record<string, string> = {
2814
[ATTR_SERVICE_NAME]: "opencode",
@@ -55,14 +41,13 @@ export function setupOtel(
5541
logsInterval: number,
5642
version: string,
5743
): OtelProviders {
58-
const headers = parseHeaders(process.env["OTEL_EXPORTER_OTLP_HEADERS"])
5944
const resource = buildResource(version)
6045

6146
const meterProvider = new MeterProvider({
6247
resource,
6348
readers: [
6449
new PeriodicExportingMetricReader({
65-
exporter: new OTLPMetricExporter({ url: endpoint, headers }),
50+
exporter: new OTLPMetricExporter({ url: endpoint }),
6651
exportIntervalMillis: metricsInterval,
6752
}),
6853
],
@@ -72,7 +57,7 @@ export function setupOtel(
7257
const loggerProvider = new LoggerProvider({
7358
resource,
7459
processors: [
75-
new BatchLogRecordProcessor(new OTLPLogExporter({ url: endpoint, headers }), {
60+
new BatchLogRecordProcessor(new OTLPLogExporter({ url: endpoint }), {
7661
scheduledDelayMillis: logsInterval,
7762
}),
7863
],

tests/config.test.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ describe("parseEnvInt", () => {
3838
describe("loadConfig", () => {
3939
const vars = [
4040
"OPENCODE_ENABLE_TELEMETRY",
41-
"OTEL_EXPORTER_OTLP_ENDPOINT",
42-
"OTEL_METRIC_EXPORT_INTERVAL",
43-
"OTEL_LOGS_EXPORT_INTERVAL",
41+
"OPENCODE_OTLP_ENDPOINT",
42+
"OPENCODE_OTLP_METRICS_INTERVAL",
43+
"OPENCODE_OTLP_LOGS_INTERVAL",
44+
"OPENCODE_OTLP_HEADERS",
45+
"OPENCODE_RESOURCE_ATTRIBUTES",
46+
"OTEL_EXPORTER_OTLP_HEADERS",
47+
"OTEL_RESOURCE_ATTRIBUTES",
4448
]
4549
beforeEach(() => vars.forEach((k) => delete process.env[k]))
4650
afterEach(() => vars.forEach((k) => delete process.env[k]))
@@ -59,25 +63,43 @@ describe("loadConfig", () => {
5963
})
6064

6165
test("reads custom endpoint", () => {
62-
process.env["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://collector:4317"
66+
process.env["OPENCODE_OTLP_ENDPOINT"] = "http://collector:4317"
6367
expect(loadConfig().endpoint).toBe("http://collector:4317")
6468
})
6569

6670
test("reads custom intervals", () => {
67-
process.env["OTEL_METRIC_EXPORT_INTERVAL"] = "30000"
68-
process.env["OTEL_LOGS_EXPORT_INTERVAL"] = "2000"
71+
process.env["OPENCODE_OTLP_METRICS_INTERVAL"] = "30000"
72+
process.env["OPENCODE_OTLP_LOGS_INTERVAL"] = "2000"
6973
const cfg = loadConfig()
7074
expect(cfg.metricsInterval).toBe(30000)
7175
expect(cfg.logsInterval).toBe(2000)
7276
})
7377

7478
test("falls back to defaults for invalid interval values", () => {
75-
process.env["OTEL_METRIC_EXPORT_INTERVAL"] = "notanumber"
76-
process.env["OTEL_LOGS_EXPORT_INTERVAL"] = "0"
79+
process.env["OPENCODE_OTLP_METRICS_INTERVAL"] = "notanumber"
80+
process.env["OPENCODE_OTLP_LOGS_INTERVAL"] = "0"
7781
const cfg = loadConfig()
7882
expect(cfg.metricsInterval).toBe(60000)
7983
expect(cfg.logsInterval).toBe(5000)
8084
})
85+
86+
test("copies OPENCODE_OTLP_HEADERS to OTEL_EXPORTER_OTLP_HEADERS", () => {
87+
process.env["OPENCODE_OTLP_HEADERS"] = "api-key=abc123"
88+
loadConfig()
89+
expect(process.env["OTEL_EXPORTER_OTLP_HEADERS"]).toBe("api-key=abc123")
90+
})
91+
92+
test("copies OPENCODE_RESOURCE_ATTRIBUTES to OTEL_RESOURCE_ATTRIBUTES", () => {
93+
process.env["OPENCODE_RESOURCE_ATTRIBUTES"] = "team=platform,env=prod"
94+
loadConfig()
95+
expect(process.env["OTEL_RESOURCE_ATTRIBUTES"]).toBe("team=platform,env=prod")
96+
})
97+
98+
test("does not set OTEL_EXPORTER_OTLP_HEADERS when OPENCODE_OTLP_HEADERS is unset", () => {
99+
delete process.env["OPENCODE_OTLP_HEADERS"]
100+
loadConfig()
101+
expect(process.env["OTEL_EXPORTER_OTLP_HEADERS"]).toBeUndefined()
102+
})
81103
})
82104

83105
describe("resolveLogLevel", () => {

tests/otel.test.ts

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,5 @@
1-
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2-
import { parseHeaders, buildResource } from "../src/otel.ts"
3-
4-
describe("parseHeaders", () => {
5-
test("returns empty object for undefined", () => {
6-
expect(parseHeaders(undefined)).toEqual({})
7-
})
8-
9-
test("returns empty object for empty string", () => {
10-
expect(parseHeaders("")).toEqual({})
11-
})
12-
13-
test("parses a single key=value pair", () => {
14-
expect(parseHeaders("api-key=abc123")).toEqual({ "api-key": "abc123" })
15-
})
16-
17-
test("parses multiple comma-separated pairs", () => {
18-
expect(parseHeaders("api-key=abc,x-tenant=foo")).toEqual({
19-
"api-key": "abc",
20-
"x-tenant": "foo",
21-
})
22-
})
23-
24-
test("trims whitespace around keys and values", () => {
25-
expect(parseHeaders(" api-key = abc123 ")).toEqual({ "api-key": "abc123" })
26-
})
27-
28-
test("ignores pairs with no = sign", () => {
29-
expect(parseHeaders("no-equals,api-key=abc")).toEqual({ "api-key": "abc" })
30-
})
31-
32-
test("handles values containing = signs", () => {
33-
expect(parseHeaders("token=abc=def")).toEqual({ token: "abc=def" })
34-
})
35-
})
1+
import { describe, test, expect, afterEach } from "bun:test"
2+
import { buildResource } from "../src/otel.ts"
363

374
describe("buildResource", () => {
385
const originalEnv = process.env["OTEL_RESOURCE_ATTRIBUTES"]

0 commit comments

Comments
 (0)