64 lines · 2.9 KB
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<string>();
  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<GithubTokenNotice | null> {
  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: [] };