Skip to content

Commit 41a86c5

Browse files
committed
Validate ANTHROPIC_API_KEY early to fail fast on missing secrets
Previously the API key check happened deep in runPipeline(), after config loading, diff parsing, trigger evaluation, and even app startup. Users had to wait through all that work before seeing the error. - Extract checkApiKey() utility with clear fail/warn/ok semantics - main action (index.ts): fail immediately after GITHUB_TOKEN check - check-trigger (check.ts): fail if should-run=true, warn if false - Keep existing check in pipeline.ts as safety net for CLI/direct usage - Add unit tests for all three checkApiKey() scenarios https://claude.ai/code/session_01Ch9jV1EASRYQZRbLwX5ofz
1 parent c2e9ebb commit 41a86c5

File tree

8 files changed

+157
-27
lines changed

8 files changed

+157
-27
lines changed

packages/action/dist/check.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19706,11 +19706,11 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
1970619706
(0, command_1.issue)("echo", enabled ? "on" : "off");
1970719707
}
1970819708
exports2.setCommandEcho = setCommandEcho;
19709-
function setFailed(message) {
19709+
function setFailed2(message) {
1971019710
process.exitCode = ExitCode.Failure;
1971119711
error(message);
1971219712
}
19713-
exports2.setFailed = setFailed;
19713+
exports2.setFailed = setFailed2;
1971419714
function isDebug() {
1971519715
return process.env["RUNNER_DEBUG"] === "1";
1971619716
}
@@ -53620,6 +53620,22 @@ function evaluateTrigger(opts) {
5362053620
};
5362153621
}
5362253622

53623+
// src/api-key-check.ts
53624+
var REMEDIATION = "Add it to your workflow: env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}";
53625+
function checkApiKey(apiKey, shouldRun) {
53626+
if (apiKey) return { action: "ok" };
53627+
if (shouldRun) {
53628+
return {
53629+
action: "fail",
53630+
message: `ANTHROPIC_API_KEY is required but not set. ${REMEDIATION}`
53631+
};
53632+
}
53633+
return {
53634+
action: "warn",
53635+
message: `ANTHROPIC_API_KEY is not set. The pipeline will fail if it runs. ${REMEDIATION}`
53636+
};
53637+
}
53638+
5362353639
// src/check.ts
5362453640
function streamCommand(cmd, args) {
5362553641
return new Promise((resolve2, reject) => {
@@ -53722,6 +53738,14 @@ async function check() {
5372253738
command
5372353739
});
5372453740
core.info(`Trigger decision: ${decision.shouldRun ? "RUN" : "SKIP"} \u2014 ${decision.reason}`);
53741+
const apiKeyCheck = checkApiKey(process.env["ANTHROPIC_API_KEY"], decision.shouldRun);
53742+
if (apiKeyCheck.action === "fail") {
53743+
core.setFailed(apiKeyCheck.message);
53744+
return;
53745+
}
53746+
if (apiKeyCheck.action === "warn") {
53747+
core.warning(apiKeyCheck.message);
53748+
}
5372553749
core.setOutput("should-run", String(decision.shouldRun));
5372653750
}
5372753751
check().catch((err) => {

packages/action/dist/check.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/action/dist/index.js

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70269,6 +70269,47 @@ function evaluateTrigger(opts) {
7026970269
};
7027070270
}
7027170271

70272+
// src/resolve-base-url.ts
70273+
function resolveBaseUrl(config, previewUrlOverride) {
70274+
const previewUrl = previewUrlOverride ?? config.app.previewUrl;
70275+
if (previewUrl) {
70276+
const resolved = process.env[previewUrl];
70277+
if (resolved === void 0) {
70278+
if (previewUrl.startsWith("http")) return { url: previewUrl };
70279+
return {
70280+
error: `app.previewUrl is set to "${previewUrl}" but it doesn't look like a URL and no env var with that name was found. Set it to a full URL (e.g. "https://my-preview.vercel.app") or an env var name that is available in this workflow job.`
70281+
};
70282+
}
70283+
if (!resolved.startsWith("http")) {
70284+
return {
70285+
error: `Env var "${previewUrl}" was found but its value "${resolved}" is not a valid URL. Expected a value starting with "http".`
70286+
};
70287+
}
70288+
return { url: resolved };
70289+
}
70290+
if (config.app.readyWhen?.url) {
70291+
const u2 = new URL(config.app.readyWhen.url);
70292+
return { url: u2.origin };
70293+
}
70294+
return { url: "http://localhost:3000" };
70295+
}
70296+
70297+
// src/api-key-check.ts
70298+
var REMEDIATION = "Add it to your workflow: env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}";
70299+
function checkApiKey(apiKey, shouldRun) {
70300+
if (apiKey) return { action: "ok" };
70301+
if (shouldRun) {
70302+
return {
70303+
action: "fail",
70304+
message: `ANTHROPIC_API_KEY is required but not set. ${REMEDIATION}`
70305+
};
70306+
}
70307+
return {
70308+
action: "warn",
70309+
message: `ANTHROPIC_API_KEY is not set. The pipeline will fail if it runs. ${REMEDIATION}`
70310+
};
70311+
}
70312+
7027270313
// src/index.ts
7027370314
function streamCommand(cmd, args) {
7027470315
return new Promise((resolve2, reject) => {
@@ -70290,6 +70331,11 @@ async function run() {
7029070331
core.setFailed("GITHUB_TOKEN is required");
7029170332
return;
7029270333
}
70334+
const apiKeyCheck = checkApiKey(process.env["ANTHROPIC_API_KEY"], true);
70335+
if (apiKeyCheck.action === "fail") {
70336+
core.setFailed(apiKeyCheck.message);
70337+
return;
70338+
}
7029370339
const eventName = context2.eventName;
7029470340
if (eventName !== "pull_request" && eventName !== "issue_comment") {
7029570341
core.info(`git-glimpse does not handle '${eventName}' events. Skipping.`);
@@ -70385,13 +70431,12 @@ async function run() {
7038570431
core.setOutput("success", "false");
7038670432
return;
7038770433
}
70388-
const baseUrl = resolveBaseUrl(config, previewUrlInput);
70389-
if (!baseUrl) {
70390-
core.setFailed(
70391-
"No base URL available. Set app.previewUrl or app.startCommand + app.readyWhen in config."
70392-
);
70434+
const baseUrlResult = resolveBaseUrl(config, previewUrlInput);
70435+
if (!baseUrlResult.url) {
70436+
core.setFailed(baseUrlResult.error);
7039370437
return;
7039470438
}
70439+
const baseUrl = baseUrlResult.url;
7039570440
if (config.setup) {
7039670441
core.info(`Running setup: ${config.setup}`);
7039770442
const parts = config.setup.split(" ");
@@ -70447,18 +70492,6 @@ ${result.errors.join("\n")}`);
7044770492
appProcess?.kill();
7044870493
}
7044970494
}
70450-
function resolveBaseUrl(config, previewUrlOverride) {
70451-
const previewUrl = previewUrlOverride ?? config.app.previewUrl;
70452-
if (previewUrl) {
70453-
const resolved = process.env[previewUrl] ?? previewUrl;
70454-
return resolved.startsWith("http") ? resolved : null;
70455-
}
70456-
if (config.app.readyWhen?.url) {
70457-
const u2 = new URL(config.app.readyWhen.url);
70458-
return u2.origin;
70459-
}
70460-
return "http://localhost:3000";
70461-
}
7046270495
async function startApp(startCommand, readyUrl) {
7046370496
const parts = startCommand.split(" ");
7046470497
core.info(`Starting app: ${startCommand}`);

packages/action/dist/index.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const REMEDIATION = 'Add it to your workflow: env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}';
2+
3+
export type ApiKeyCheckResult =
4+
| { action: 'ok' }
5+
| { action: 'fail'; message: string }
6+
| { action: 'warn'; message: string };
7+
8+
/**
9+
* Checks whether ANTHROPIC_API_KEY is present and returns what the caller
10+
* should do.
11+
*
12+
* @param shouldRun - whether the pipeline is about to run
13+
* - true → missing key is a hard failure (fail fast before expensive work)
14+
* - false → missing key is a warning only (pipeline won't run this time)
15+
*/
16+
export function checkApiKey(
17+
apiKey: string | undefined,
18+
shouldRun: boolean
19+
): ApiKeyCheckResult {
20+
if (apiKey) return { action: 'ok' };
21+
22+
if (shouldRun) {
23+
return {
24+
action: 'fail',
25+
message: `ANTHROPIC_API_KEY is required but not set. ${REMEDIATION}`,
26+
};
27+
}
28+
29+
return {
30+
action: 'warn',
31+
message: `ANTHROPIC_API_KEY is not set. The pipeline will fail if it runs. ${REMEDIATION}`,
32+
};
33+
}

packages/action/src/check.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
parseGlimpseCommand,
1818
DEFAULT_TRIGGER,
1919
} from '@git-glimpse/core';
20+
import { checkApiKey } from './api-key-check.js';
2021

2122
function streamCommand(cmd: string, args: string[]): Promise<string> {
2223
return new Promise((resolve, reject) => {
@@ -138,6 +139,16 @@ async function check(): Promise<void> {
138139
});
139140

140141
core.info(`Trigger decision: ${decision.shouldRun ? 'RUN' : 'SKIP'}${decision.reason}`);
142+
143+
const apiKeyCheck = checkApiKey(process.env['ANTHROPIC_API_KEY'], decision.shouldRun);
144+
if (apiKeyCheck.action === 'fail') {
145+
core.setFailed(apiKeyCheck.message);
146+
return;
147+
}
148+
if (apiKeyCheck.action === 'warn') {
149+
core.warning(apiKeyCheck.message);
150+
}
151+
141152
core.setOutput('should-run', String(decision.shouldRun));
142153
}
143154

packages/action/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type GitGlimpseConfig,
1515
} from '@git-glimpse/core';
1616
import { resolveBaseUrl } from './resolve-base-url.js';
17+
import { checkApiKey } from './api-key-check.js';
1718

1819
function streamCommand(cmd: string, args: string[]): Promise<string> {
1920
return new Promise((resolve, reject) => {
@@ -38,6 +39,12 @@ async function run(): Promise<void> {
3839
return;
3940
}
4041

42+
const apiKeyCheck = checkApiKey(process.env['ANTHROPIC_API_KEY'], true);
43+
if (apiKeyCheck.action === 'fail') {
44+
core.setFailed(apiKeyCheck.message);
45+
return;
46+
}
47+
4148
const eventName = context.eventName;
4249
if (eventName !== 'pull_request' && eventName !== 'issue_comment') {
4350
core.info(`git-glimpse does not handle '${eventName}' events. Skipping.`);

tests/unit/api-key-check.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { checkApiKey } from '../../packages/action/src/api-key-check.js';
3+
4+
describe('checkApiKey', () => {
5+
it('returns ok when key is present', () => {
6+
expect(checkApiKey('sk-ant-123', true)).toEqual({ action: 'ok' });
7+
expect(checkApiKey('sk-ant-123', false)).toEqual({ action: 'ok' });
8+
});
9+
10+
it('returns fail when key is missing and pipeline will run', () => {
11+
const result = checkApiKey(undefined, true);
12+
expect(result.action).toBe('fail');
13+
expect((result as { action: 'fail'; message: string }).message).toMatch(/ANTHROPIC_API_KEY/);
14+
expect((result as { action: 'fail'; message: string }).message).toMatch(/secrets\.ANTHROPIC_API_KEY/);
15+
});
16+
17+
it('returns warn when key is missing but pipeline will not run', () => {
18+
const result = checkApiKey(undefined, false);
19+
expect(result.action).toBe('warn');
20+
expect((result as { action: 'warn'; message: string }).message).toMatch(/ANTHROPIC_API_KEY/);
21+
});
22+
});

0 commit comments

Comments
 (0)