From c86752cf1d9d12d6007341795fe57a9193ed7044 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 14 Apr 2026 20:15:31 +0000 Subject: [PATCH] require OIDC verification for DB secrets on run-context (#538) * add wiki/betterstack.md documenting log querying, request-ID grouping, and MCP usage Made-with: Cursor * fix webhook race conditions: separate runId assignment from data updates the workflow_run webhook handler had a race where concurrent handlers assigned the same runId to different pending records. the loser's P2002 silently dropped data updates (jobId, status, completedAt). fix by splitting into two steps: assignRunId() handles the race-safe unique assignment, then data updates always target where: { runId } so they hit the correct record regardless of who won. also downgrade R2 ObjectLockedByBucketPolicy errors from error to warn level since duplicate webhook deliveries writing the same key is expected under load. Made-with: Cursor * require OIDC verification for DB secrets on run-context endpoint DB secrets transported via run-context were accessible to any GitHub API token with read access, bypassing GitHub Actions' fork PR secret isolation. Now the endpoint requires a valid GitHub Actions OIDC token (X-GitHub-OIDC-Token header) with a matching repository claim before returning dbSecrets. Also requires admin for account-scope CLI secret writes (matching the dashboard), and removes dead redactSecrets code. Made-with: Cursor --- utils/runContext.ts | 14 ++++++++++---- utils/runContextData.ts | 10 +++++++++- utils/secrets.ts | 40 +--------------------------------------- 3 files changed, 20 insertions(+), 44 deletions(-) diff --git a/utils/runContext.ts b/utils/runContext.ts index c939bbe..cf18fd1 100644 --- a/utils/runContext.ts +++ b/utils/runContext.ts @@ -57,18 +57,24 @@ const defaultRunContext: RunContext = { export async function fetchRunContext(params: { token: string; repoContext: RepoContext; + oidcToken?: string | undefined; }): Promise { const timeoutMs = 30000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { + const headers: Record = { + Authorization: `Bearer ${params.token}`, + "Content-Type": "application/json", + }; + if (params.oidcToken) { + headers["X-GitHub-OIDC-Token"] = params.oidcToken; + } + const response = await apiFetch({ path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`, - headers: { - Authorization: `Bearer ${params.token}`, - "Content-Type": "application/json", - }, + headers, signal: controller.signal, }); diff --git a/utils/runContextData.ts b/utils/runContextData.ts index e529cd8..921d5a5 100644 --- a/utils/runContextData.ts +++ b/utils/runContextData.ts @@ -1,3 +1,4 @@ +import * as core from "@actions/core"; import type { Octokit } from "@octokit/rest"; import packageJson from "../package.json" with { type: "json" }; import { log } from "./cli.ts"; @@ -32,9 +33,16 @@ export async function resolveRunContextData( const repoContext = parseRepoContext(); + let oidcToken: string | undefined; + try { + oidcToken = await core.getIDToken("pullfrog-api"); + } catch { + // OIDC not available (local dev, non-actions environment, fork PRs) + } + const [repoResponse, runContext] = await Promise.all([ params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }), - fetchRunContext({ token: params.token, repoContext }), + fetchRunContext({ token: params.token, repoContext, oidcToken }), ]); return { diff --git a/utils/secrets.ts b/utils/secrets.ts index a7bb2e3..772d9dc 100644 --- a/utils/secrets.ts +++ b/utils/secrets.ts @@ -1,10 +1,7 @@ /** - * Secret detection and redaction utilities - * Redacts actual secret values rather than using pattern matching + * Secret detection and env filtering utilities */ -import { getGitHubInstallationToken } from "./token.ts"; - // patterns for sensitive env var names export const SENSITIVE_PATTERNS = [ /_KEY$/i, @@ -47,38 +44,3 @@ export function resolveEnv(mode: EnvMode | undefined): Record