import { isBuiltinAction } from "./builtin-actions"; import { GITHUB_COM_TOKEN_SECRET } from "./actions/compile-action"; import { loadSecretRows } from "./config-values"; import { parseWorkflow, WORKFLOW_DIR } from "./spec"; import type { WorkflowSpec } from "./spec"; import { listRefs, resolveRevision } from "../git/refs"; import { withStore } from "../git/store"; import type { RepoRow } from "~/lib/types"; const TOKEN_EXPRESSION = /\$\{\{\s*(?:github\.token|secrets\.GITHUB_TOKEN)\s*\}\}/; const MAX_LISTED = 10; /** Actions whose `with:` passes `github.token` / `secrets.GITHUB_TOKEN`, which gitfor maps to GITHUB_COM_TOKEN. */ export function actionsWantingGithubToken(spec: WorkflowSpec): string[] { const uses = new Set(); for (const job of Object.values(spec.jobs)) { for (const step of job.steps) { if (step.uses === undefined || isBuiltinAction(step)) continue; if (Object.values(step.with ?? {}).some((value) => TOKEN_EXPRESSION.test(String(value)))) { uses.add(step.uses.trim()); } } } return [...uses]; } export type GithubTokenNotice = { secret: string; actions: string[]; workflows: string[] }; /** * Workflows on the default branch whose actions expect a github.com token, when neither the repository nor its * owner has a GITHUB_COM_TOKEN secret: those actions run anonymously against api.github.com and hit rate limits * on shared runner IPs, which otherwise only shows up as a failed step at runtime. */ export async function githubTokenNotice(repo: RepoRow): Promise { const revision = resolveRevision(await listRefs(repo), repo.default_branch); if (!revision) return null; const found = await withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(revision.sha); const directory = commit ? await store.entryAtPath(commit.treeSha, WORKFLOW_DIR) : null; if (!directory || directory.type !== "tree") return { actions: [], workflows: [] }; const actions = new Set(); const workflows: string[] = []; for (const entry of await store.listTree(directory.sha, WORKFLOW_DIR)) { if (entry.type !== "blob" || !/\.ya?ml$/.test(entry.name)) continue; const bytes = await store.blob(entry.sha); const parsed = bytes ? parseWorkflow(new TextDecoder().decode(bytes)) : null; if (!parsed?.ok) continue; const wanting = actionsWantingGithubToken(parsed.spec); if (wanting.length === 0) continue; workflows.push(entry.name); for (const uses of wanting) actions.add(uses); } return { actions: [...actions], workflows }; }); if (found.actions.length === 0) return null; const secrets = await loadSecretRows(repo, null); if (secrets.some((row) => row.name === GITHUB_COM_TOKEN_SECRET)) return null; return { secret: GITHUB_COM_TOKEN_SECRET, actions: found.actions.slice(0, MAX_LISTED), workflows: found.workflows.slice(0, MAX_LISTED), }; }