surface BYOK failures + chronic-failures card + WorkflowRunStatus mirrors GitHub conclusions (#722)
- Migrates `WorkflowRunStatus` from `running | completed | cancelled` to a 9-state mirror of `workflow_run.conclusion`. Backfill: old `completed → success`, `cancelled → failure`. New rows write `hook.workflow_run.conclusion` verbatim via `statusFromConclusion`. - Adds Discord links to `formatApiKeyErrorSummary` (both missing-key and 401 invalid-key shapes). - Repo console: `<ChronicFailuresCard>` fires when the last 3 terminal-state runs are all `failure`. Pure DB read; latest-run button hidden for pre-dispatch failures (`runId: null`). - `StatusIcon` distinguishes `cancelled` (gray X, intentional stop) from `failure` (red X) so the visual matches the chronic-card threshold. - Pre-dispatch failures (workflow lookup miss, dispatch API error) write `failure` instead of `cancelled` so they feed the card. - Cascade: every `status: "completed"` filter in billing routes / cron / cohort queries / analyzer becomes `status: "success"`. Verified end-to-end on `pullfrog/preview-722-failure-surfaces` — Better Stack logs confirm webhooks reached the preview deploy and all three e2e runs got `marked as failure (conclusion=failure)` via the new mapper. Closes #679, #702.
This commit is contained in:
committed by
pullfrog[bot]
parent
206c11fe7c
commit
d495f0b984
+46
-1
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { validateAgentApiKey } from "./apiKeys.ts";
|
||||
import { formatApiKeyErrorSummary, isApiKeyAuthError, validateAgentApiKey } from "./apiKeys.ts";
|
||||
|
||||
const base = {
|
||||
agent: { name: "opencode" },
|
||||
@@ -156,3 +156,48 @@ describe("validateAgentApiKey", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isApiKeyAuthError", () => {
|
||||
it("matches the missing-key marker thrown by validateAgentApiKey", () => {
|
||||
expect(isApiKeyAuthError("no API key found. Pullfrog needs ...")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches Claude CLI 401 strings", () => {
|
||||
expect(isApiKeyAuthError("Invalid API key · Fix external API key")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches OpenAI / OpenRouter 401 phrasings", () => {
|
||||
expect(isApiKeyAuthError("ProviderAuthError: User not found")).toBe(true);
|
||||
expect(isApiKeyAuthError("401 Invalid authentication")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores unrelated errors", () => {
|
||||
expect(isApiKeyAuthError("git fetch failed")).toBe(false);
|
||||
expect(isApiKeyAuthError("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatApiKeyErrorSummary", () => {
|
||||
it("renders the missing-key body when the raw error contains the marker", () => {
|
||||
const msg = formatApiKeyErrorSummary({
|
||||
owner: "acme",
|
||||
name: "repo",
|
||||
raw: "no API key found in this run",
|
||||
});
|
||||
expect(msg).toContain("no API key found");
|
||||
expect(msg).toContain("https://github.com/acme/repo/settings/secrets/actions");
|
||||
expect(msg).toContain("/console/acme/repo");
|
||||
expect(msg).toContain("https://discord.gg/8y96raFg8e");
|
||||
});
|
||||
|
||||
it("renders the invalid-key body for any other auth error", () => {
|
||||
const msg = formatApiKeyErrorSummary({
|
||||
owner: "acme",
|
||||
name: "repo",
|
||||
raw: "Invalid API key · Fix external API key",
|
||||
});
|
||||
expect(msg).toContain("rejected (401)");
|
||||
expect(msg).toContain("https://github.com/acme/repo/settings/secrets/actions");
|
||||
expect(msg).toContain("https://discord.gg/8y96raFg8e");
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ function buildMissingApiKeyError(params: { owner: string; name: string }): strin
|
||||
return [
|
||||
`**${MISSING_KEY_MARKER}** — Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`,
|
||||
"",
|
||||
`[Open repo secrets →](${githubSecretsUrl}) · [Configure model →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys)`,
|
||||
`[Open repo secrets →](${githubSecretsUrl}) · [Configure model →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys) · [Ask in Discord →](https://discord.gg/8y96raFg8e)`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -153,6 +153,6 @@ export function formatApiKeyErrorSummary(params: {
|
||||
return [
|
||||
`**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
||||
"",
|
||||
`[Update repo secret →](${githubSecretsUrl}) · [Model settings →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys)`,
|
||||
`[Update repo secret →](${githubSecretsUrl}) · [Model settings →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys) · [Ask in Discord →](https://discord.gg/8y96raFg8e)`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user