Refactor (#109)
This commit is contained in:
committed by
pullfrog[bot]
parent
101c666610
commit
69b9b96ddd
+1
-11
@@ -1,10 +1,5 @@
|
||||
import type { Agent } from "../agents/index.ts";
|
||||
|
||||
export interface ApiKeySetup {
|
||||
apiKey: string;
|
||||
apiKeys: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a helpful error message for missing API key with links to repo settings
|
||||
*/
|
||||
@@ -61,7 +56,7 @@ function collectApiKeys(agent: Agent): Record<string, string> {
|
||||
return apiKeys;
|
||||
}
|
||||
|
||||
export function validateApiKey(params: { agent: Agent; owner: string; name: string }): ApiKeySetup {
|
||||
export function validateApiKey(params: { agent: Agent; owner: string; name: string }): void {
|
||||
const apiKeys = collectApiKeys(params.agent);
|
||||
|
||||
if (Object.keys(apiKeys).length === 0) {
|
||||
@@ -73,9 +68,4 @@ export function validateApiKey(params: { agent: Agent; owner: string; name: stri
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: Object.values(apiKeys)[0],
|
||||
apiKeys,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ ${getShellInstructions(input.bash)}
|
||||
|
||||
### Available modes
|
||||
|
||||
${[...computeModes({ disableProgressComment: false }), ...input.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||
${[...computeModes({ hasProgressComment: true }), ...input.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||
|
||||
### Following the mode instructions
|
||||
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { log } from "./cli.ts";
|
||||
|
||||
// patterns for sensitive env vars: suffixes (_KEY, _SECRET, _TOKEN) plus AI provider prefixes
|
||||
const SENSITIVE_PATTERNS = [/_KEY$/i, /_SECRET$/i, /_TOKEN$/i, /_PASSWORD$/i, /_CREDENTIAL$/i];
|
||||
|
||||
function isSensitive(key: string): boolean {
|
||||
return SENSITIVE_PATTERNS.some((p) => p.test(key));
|
||||
}
|
||||
|
||||
function maskValue(value: string | undefined) {
|
||||
if (value && typeof value === "string" && value.trim().length > 0) {
|
||||
// ::add-mask::value tells GitHub Actions to mask this value in logs
|
||||
console.log(`::add-mask::${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize environment variables to uppercase.
|
||||
* This handles case-insensitive env var names (e.g., `anthropic_api_key` -> `ANTHROPIC_API_KEY`).
|
||||
*
|
||||
* If there are conflicts (same key with different capitalizations but different values),
|
||||
* logs a warning and keeps the uppercase version.
|
||||
*
|
||||
* Also registers sensitive values as masks in GitHub Actions.
|
||||
*/
|
||||
export function normalizeEnv(): void {
|
||||
const upperKeys = new Map<string, string[]>();
|
||||
@@ -20,6 +36,14 @@ export function normalizeEnv(): void {
|
||||
|
||||
// process each group
|
||||
for (const [upperKey, keys] of upperKeys) {
|
||||
// if sensitive, ensure we mask the value (regardless of whether we rename it or not)
|
||||
if (isSensitive(upperKey)) {
|
||||
// mask all values associated with this key group
|
||||
for (const key of keys) {
|
||||
maskValue(process.env[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (keys.length === 1) {
|
||||
const key = keys[0];
|
||||
if (key !== upperKey) {
|
||||
|
||||
+10
-16
@@ -1,4 +1,5 @@
|
||||
import { isAbsolute, resolve } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { type } from "arktype";
|
||||
import {
|
||||
AgentName,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
Effort,
|
||||
type PayloadEvent,
|
||||
} from "../external.ts";
|
||||
import type { RepoSettings } from "./repoSettings.ts";
|
||||
|
||||
// tool permission enum types for inputs
|
||||
const ToolPermissionInput = type.enumerated("disabled", "enabled");
|
||||
@@ -22,10 +24,6 @@ const JsonPayload = type({
|
||||
"search?": ToolPermissionInput,
|
||||
"write?": ToolPermissionInput,
|
||||
"bash?": BashPermissionInput,
|
||||
"disableProgressComment?": "true",
|
||||
"comment_id?": "number|null",
|
||||
"issue_id?": "number|null",
|
||||
"pr_id?": "number|null",
|
||||
});
|
||||
|
||||
// inputs schema - action inputs from core.getInput()
|
||||
@@ -61,9 +59,7 @@ function resolveCwd(cwd: string | null | undefined): string | null {
|
||||
return workspace ? resolve(workspace, cwd) : cwd;
|
||||
}
|
||||
|
||||
export function resolvePayload(core: {
|
||||
getInput: (name: string, options?: { required?: boolean }) => string;
|
||||
}) {
|
||||
export function resolvePayload(repoSettings: RepoSettings) {
|
||||
const inputs = Inputs.assert({
|
||||
prompt: core.getInput("prompt", { required: true }),
|
||||
effort: core.getInput("effort") || "auto",
|
||||
@@ -107,7 +103,7 @@ export function resolvePayload(core: {
|
||||
agent ??
|
||||
(jsonAgent !== undefined && jsonAgent !== "null" && isAgentName(jsonAgent) ? jsonAgent : null);
|
||||
|
||||
// build payload - precedence: inputs > jsonPayload > defaults
|
||||
// build payload - precedence: inputs > jsonPayload > repoSettings > fallbacks
|
||||
// note: modes are NOT in payload - they come from repoSettings in main()
|
||||
return {
|
||||
"~pullfrog": true as const,
|
||||
@@ -115,15 +111,13 @@ export function resolvePayload(core: {
|
||||
prompt: inputs.prompt ?? jsonPayload?.prompt,
|
||||
event,
|
||||
effort: inputs.effort ?? jsonPayload?.effort ?? "auto",
|
||||
web: inputs.web ?? jsonPayload?.web,
|
||||
search: inputs.search ?? jsonPayload?.search,
|
||||
write: inputs.write ?? jsonPayload?.write,
|
||||
bash: inputs.bash ?? jsonPayload?.bash,
|
||||
disableProgressComment: jsonPayload?.disableProgressComment === true,
|
||||
comment_id: jsonPayload?.comment_id ?? null,
|
||||
issue_id: jsonPayload?.issue_id ?? null,
|
||||
pr_id: jsonPayload?.pr_id ?? null,
|
||||
cwd: resolveCwd(inputs.cwd),
|
||||
|
||||
// permissions: inputs > jsonPayload > repoSettings > fallbacks
|
||||
web: inputs.web ?? jsonPayload?.web ?? repoSettings.web ?? "enabled",
|
||||
search: inputs.search ?? jsonPayload?.search ?? repoSettings.search ?? "enabled",
|
||||
write: inputs.write ?? jsonPayload?.write ?? repoSettings.write ?? "enabled",
|
||||
bash: inputs.bash ?? jsonPayload?.bash ?? repoSettings.bash ?? "restricted",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+13
-9
@@ -1,38 +1,42 @@
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { log } from "./cli.ts";
|
||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
||||
import { createOctokit, type OctokitWithPlugins, parseRepoContext } from "./github.ts";
|
||||
import { fetchRepoSettings, type RepoSettings } from "./repoSettings.ts";
|
||||
|
||||
export interface RepoData {
|
||||
owner: string;
|
||||
name: string;
|
||||
octokit: Octokit;
|
||||
repo: Awaited<ReturnType<Octokit["repos"]["get"]>>["data"];
|
||||
repoSettings: RepoSettings;
|
||||
}
|
||||
|
||||
interface ResolveRepoDataParams {
|
||||
octokit: OctokitWithPlugins;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize GitHub connection: token, octokit, repo data, settings
|
||||
* Initialize repo data: parse context, fetch repo info and settings
|
||||
*/
|
||||
export async function resolveRepoData(token: string): Promise<RepoData> {
|
||||
export async function resolveRepoData(params: ResolveRepoDataParams): Promise<RepoData> {
|
||||
log.info(`» running Pullfrog v${packageJson.version}...`);
|
||||
|
||||
const { owner, name } = parseRepoContext();
|
||||
|
||||
const octokit = createOctokit(token);
|
||||
|
||||
// fetch repo data and settings in parallel
|
||||
const [repoResponse, repoSettings] = await Promise.all([
|
||||
octokit.repos.get({ owner, repo: name }),
|
||||
fetchRepoSettings({ token, repoContext: { owner, name } }),
|
||||
params.octokit.repos.get({ owner, repo: name }),
|
||||
fetchRepoSettings({ token: params.token, repoContext: { owner, name } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
owner,
|
||||
name,
|
||||
octokit,
|
||||
repo: repoResponse.data,
|
||||
repoSettings,
|
||||
};
|
||||
}
|
||||
|
||||
// re-export for convenience
|
||||
export { createOctokit };
|
||||
|
||||
+25
-13
@@ -10,22 +10,13 @@ export interface Mode {
|
||||
|
||||
export interface RepoSettings {
|
||||
defaultAgent: AgentName | null;
|
||||
modes: Mode[];
|
||||
web: ToolPermission;
|
||||
search: ToolPermission;
|
||||
write: ToolPermission;
|
||||
bash: BashPermission;
|
||||
modes: Mode[];
|
||||
}
|
||||
|
||||
export const DEFAULT_REPO_SETTINGS: RepoSettings = {
|
||||
defaultAgent: null,
|
||||
web: "enabled",
|
||||
search: "enabled",
|
||||
write: "enabled",
|
||||
bash: "restricted",
|
||||
modes: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch repository settings from the Pullfrog API
|
||||
* Returns defaults if repo doesn't exist or fetch fails
|
||||
@@ -55,17 +46,38 @@ export async function fetchRepoSettings(params: {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return DEFAULT_REPO_SETTINGS;
|
||||
return {
|
||||
defaultAgent: null,
|
||||
modes: [],
|
||||
web: "enabled",
|
||||
search: "enabled",
|
||||
write: "enabled",
|
||||
bash: "restricted",
|
||||
};
|
||||
}
|
||||
|
||||
const settings = (await response.json()) as RepoSettings | null;
|
||||
if (settings === null) {
|
||||
return DEFAULT_REPO_SETTINGS;
|
||||
return {
|
||||
defaultAgent: null,
|
||||
modes: [],
|
||||
web: "enabled",
|
||||
search: "enabled",
|
||||
write: "enabled",
|
||||
bash: "restricted",
|
||||
};
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch {
|
||||
clearTimeout(timeoutId);
|
||||
return DEFAULT_REPO_SETTINGS;
|
||||
return {
|
||||
defaultAgent: null,
|
||||
modes: [],
|
||||
web: "enabled",
|
||||
search: "enabled",
|
||||
write: "enabled",
|
||||
bash: "restricted",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+1
-18
@@ -1,23 +1,6 @@
|
||||
import type { AgentResult, ToolPermissions } from "../agents/shared.ts";
|
||||
import type { AgentResult } from "../agents/shared.ts";
|
||||
import type { MainResult } from "../main.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import type { ResolvedPayload } from "./payload.ts";
|
||||
|
||||
/**
|
||||
* Compute tool permissions from inputs.
|
||||
* For run action, bash defaults to restricted for public repos when unset.
|
||||
*/
|
||||
export function resolvePermissions(params: {
|
||||
payload: ResolvedPayload;
|
||||
isPublicRepo: boolean;
|
||||
}): ToolPermissions {
|
||||
return {
|
||||
web: params.payload.web ?? "enabled",
|
||||
search: params.payload.search ?? "enabled",
|
||||
write: params.payload.write ?? "enabled",
|
||||
bash: params.payload.bash ?? (params.isPublicRepo ? "restricted" : "enabled"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleAgentResult(result: AgentResult): Promise<MainResult> {
|
||||
if (!result.success) {
|
||||
|
||||
+29
-16
@@ -1,29 +1,42 @@
|
||||
import { log } from "./cli.ts";
|
||||
import type { RepoData } from "./repoData.ts";
|
||||
import { fetchWorkflowRunInfo } from "./workflowRun.ts";
|
||||
import type { OctokitWithPlugins } from "./github.ts";
|
||||
import { fetchWorkflowRunInfo, type WorkflowRunInfo } from "./workflowRun.ts";
|
||||
|
||||
interface ResolveRunParams {
|
||||
octokit: OctokitWithPlugins;
|
||||
}
|
||||
|
||||
export interface ResolveRunResult {
|
||||
runId: string;
|
||||
jobId: string | undefined;
|
||||
workflowRunInfo: WorkflowRunInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve GitHub Actions workflow run context (runId, jobId, progress comment)
|
||||
* Resolve GitHub Actions workflow run context.
|
||||
* Uses GITHUB_REPOSITORY and GITHUB_RUN_ID env vars.
|
||||
*/
|
||||
export async function resolveRunId(
|
||||
repoData: RepoData
|
||||
): Promise<{ runId: string; jobId: string | undefined }> {
|
||||
export async function resolveRun(params: ResolveRunParams): Promise<ResolveRunResult> {
|
||||
const runId = process.env.GITHUB_RUN_ID || "";
|
||||
const githubRepo = process.env.GITHUB_REPOSITORY;
|
||||
if (!githubRepo || !githubRepo.includes("/")) {
|
||||
throw new Error(`GITHUB_REPOSITORY env var must be set to "owner/repo", got: ${githubRepo}`);
|
||||
}
|
||||
const [owner, repo] = githubRepo.split("/");
|
||||
|
||||
if (runId) {
|
||||
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||
if (workflowRunInfo.progressCommentId) {
|
||||
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||
log.info(`» using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
|
||||
}
|
||||
const workflowRunInfo = runId ? await fetchWorkflowRunInfo(runId) : { progressCommentId: null };
|
||||
|
||||
if (workflowRunInfo.progressCommentId) {
|
||||
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||
log.info(`» using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
|
||||
}
|
||||
|
||||
let jobId: string | undefined;
|
||||
const jobName = process.env.GITHUB_JOB;
|
||||
if (jobName && runId) {
|
||||
const jobs = await repoData.octokit.rest.actions.listJobsForWorkflowRun({
|
||||
owner: repoData.owner,
|
||||
repo: repoData.name,
|
||||
const jobs = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: parseInt(runId, 10),
|
||||
});
|
||||
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
|
||||
@@ -33,5 +46,5 @@ export async function resolveRunId(
|
||||
}
|
||||
}
|
||||
|
||||
return { runId, jobId };
|
||||
return { runId, jobId, workflowRunInfo };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export interface WorkflowRunInfo {
|
||||
progressCommentId: string | null;
|
||||
issueNumber: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workflow run info from the Pullfrog API
|
||||
* Returns the pre-created progress comment ID if one exists
|
||||
* Fetch workflow run info from the Pullfrog API.
|
||||
* Returns the pre-created progress comment ID if one exists.
|
||||
*/
|
||||
export async function fetchWorkflowRunInfo(runId: string): Promise<WorkflowRunInfo> {
|
||||
const apiUrl = process.env.API_URL || "https://pullfrog.com";
|
||||
@@ -25,13 +24,13 @@ export async function fetchWorkflowRunInfo(runId: string): Promise<WorkflowRunIn
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return { progressCommentId: null, issueNumber: null };
|
||||
return { progressCommentId: null };
|
||||
}
|
||||
|
||||
const data = (await response.json()) as WorkflowRunInfo;
|
||||
return data;
|
||||
} catch {
|
||||
clearTimeout(timeoutId);
|
||||
return { progressCommentId: null, issueNumber: null };
|
||||
return { progressCommentId: null };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user