From a085bf62a459c21d90a89bcae006e969df60d944 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 2 Sep 2026 17:24:53 -0400 Subject: [PATCH] feat(updates): log requests to the shared data lake --- .github/workflows/publish.yml | 2 +- packages/updates/README.md | 20 ++++++ packages/updates/src/index.test.ts | 110 ----------------------------- packages/updates/src/index.ts | 20 +++++- packages/updates/wrangler.jsonc | 7 ++ 5 files changed, 47 insertions(+), 112 deletions(-) delete mode 100644 packages/updates/src/index.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fa785a0f472..46151303280 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,7 +47,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: Deploy update service - if: github.ref_name == 'v2' || github.ref_name == 'beta' + if: github.ref_name == 'v2' working-directory: packages/updates run: bun run deploy env: diff --git a/packages/updates/README.md b/packages/updates/README.md index e4ca686b404..28594eb36b9 100644 --- a/packages/updates/README.md +++ b/packages/updates/README.md @@ -16,6 +16,26 @@ The `/admin*` route must be protected by a Cloudflare Access self-hosted applica The Worker has `workers_dev` and preview URLs disabled so the custom hostname is its only public entry point. +## Request logging + +Every request reaching the Worker emits an unsampled event at request start to the shared production +Cloudflare lake stream through the `EVENTS` Pipelines binding. Events use +`source: "update"`, `type: "request"`, an ISO `timestamp`, and a `payload` containing +the method, path, `user_agent`, country, +and Cloudflare colo. Query strings, request bodies, cookies, authorization headers, +and IP addresses are not included. Response status and duration are not recorded. + +Delivery runs in `waitUntil` without delaying the response. Delivery failures are +logged but do not fail requests or retry; this is not lossless audit logging. +Requests blocked before reaching the Worker are not recorded. + +The stream ID in `wrangler.jsonc` comes from the `lake.stream` output of the +`anomalyco/platform/production` Pulumi stack. Update the binding if that stream is +replaced. The stream is shared across release channels because the update service +has a single public deployment. + +## Publishing + GitHub Actions publishes artifacts through `POST /api/publish` using a short-lived OIDC token with audience `https://update.opencode.ai`. The Worker accepts only tokens signed by GitHub for repository ID `975734319`, owner ID `66570915`, and `.github/workflows/publish.yml` on configured publishing refs. Apply migrations and deploy from this directory: diff --git a/packages/updates/src/index.test.ts b/packages/updates/src/index.test.ts deleted file mode 100644 index 5d5e7ebf91d..00000000000 --- a/packages/updates/src/index.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, expect, test } from "bun:test" -import worker, { channelsForRef, resolveChannel, validGitHubClaims } from "./index" - -const claims = { - repository: "anomalyco/opencode", - repository_id: "975734319", - repository_owner_id: "66570915", - workflow_ref: "anomalyco/opencode/.github/workflows/publish.yml@refs/heads/dev", - ref: "refs/heads/dev", - sha: "abc123", - run_id: "123", - run_attempt: "1", - actor: "opencode-agent", -} - -describe("GitHub publish authorization", () => { - test("allows the publish workflow from the repository", () => { - expect(validGitHubClaims(claims)).toBe(true) - expect(channelsForRef(claims.ref)).toEqual(["dev", "latest"]) - }) - - test("maps V2 development to the dev channel", () => { - expect(channelsForRef("refs/heads/v2")).toEqual(["dev"]) - }) - - test("rejects another repository or workflow", () => { - expect(validGitHubClaims({ ...claims, repository_id: "1" })).toBe(false) - expect( - validGitHubClaims({ ...claims, workflow_ref: "anomalyco/opencode/.github/workflows/other.yml@refs/heads/dev" }), - ).toBe(false) - }) - - test("rejects unconfigured refs", () => { - const ref = "refs/heads/untrusted" - expect( - validGitHubClaims({ ...claims, ref, workflow_ref: `anomalyco/opencode/.github/workflows/publish.yml@${ref}` }), - ).toBe(false) - }) -}) - -test("routes the retired next channel to beta", () => { - expect(resolveChannel("next")).toBe("beta") - expect(resolveChannel("dev")).toBe("dev") -}) - -const artifact = { - channel: "beta", - name: "opencode", - distribution: "darwin-arm64", - version: "1.0.0", - metadata: "{}", - active: 1, - time_created: 1, - time_updated: 2, -} - -test.each([ - ["/api/next", ["beta"], { channel: "beta", artifacts: [{ ...artifact, metadata: {}, active: true }] }], - [ - "/api/next/opencode", - ["beta", "opencode"], - { channel: "beta", name: "opencode", artifacts: [{ ...artifact, metadata: {}, active: true }] }, - ], - [ - "/api/next/opencode/darwin-arm64", - ["beta", "opencode", "darwin-arm64"], - { ...artifact, metadata: {}, active: true }, - ], -])("routes GET %s", async (path, expectedBindings, expectedBody) => { - const bindings: unknown[][] = [] - const statement = { - bind(...values: unknown[]) { - bindings.push(values) - return statement - }, - async all() { - return { results: [artifact] } - }, - async first() { - return artifact - }, - } - const db = { - prepare() { - return statement - }, - } as unknown as D1Database - - const response = await worker.fetch(new Request(`https://update.opencode.ai${path}`), { DB: db }) - - expect(response.status).toBe(200) - expect(await response.text()).toBe(JSON.stringify(expectedBody)) - expect(bindings).toEqual([expectedBindings]) -}) - -test.each(["/api", "/v1/dev", "/api/dev/opencode/darwin-arm64/extra", "/api/dev/opencode/darwin$arm64"])( - "returns 404 for GET %s", - async (path) => { - const db = { - prepare() { - throw new Error("Invalid routes must not query the database") - }, - } as unknown as D1Database - - const response = await worker.fetch(new Request(`https://update.opencode.ai${path}`), { DB: db }) - - expect(response.status).toBe(404) - expect(await response.text()).toBe("Not found") - }, -) diff --git a/packages/updates/src/index.ts b/packages/updates/src/index.ts index fdef7ecfa36..429874c8cb2 100644 --- a/packages/updates/src/index.ts +++ b/packages/updates/src/index.ts @@ -1,7 +1,9 @@ import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose" +import type { Pipeline } from "cloudflare:pipelines" interface Env { DB: D1Database + EVENTS: Pipeline } type ArtifactRow = { @@ -31,8 +33,24 @@ const audience = "https://update.opencode.ai" const githubKeys = createRemoteJWKSet(new URL("https://token.actions.githubusercontent.com/.well-known/jwks")) export default { - async fetch(request: Request, env: Env): Promise { + async fetch(request: Request, env: Env, ctx: Pick): Promise { const url = new URL(request.url) + ctx.waitUntil( + env.EVENTS.send([ + { + source: "update", + type: "request", + timestamp: new Date().toISOString(), + payload: { + method: request.method, + path: url.pathname, + user_agent: request.headers.get("user-agent"), + cf_country: request.cf?.country, + cf_colo: request.cf?.colo, + }, + }, + ]).catch(() => console.error("Failed to send update request event to the data lake")), + ) if (url.pathname === "/") return json({ service: "opencode-updates" }) if (url.pathname === "/admin" && request.method === "GET") return admin(request, env) diff --git a/packages/updates/wrangler.jsonc b/packages/updates/wrangler.jsonc index 6e41a4c7afe..5f8249470f8 100644 --- a/packages/updates/wrangler.jsonc +++ b/packages/updates/wrangler.jsonc @@ -5,6 +5,13 @@ "compatibility_date": "2026-07-21", "workers_dev": false, "preview_urls": false, + "pipelines": [ + { + "binding": "EVENTS", + // Shared lake stream from anomalyco/platform/production. + "stream": "251a89241c3a461c9007f6b6f345ed8b", + }, + ], "routes": [ { "pattern": "update.opencode.ai",