/** A parsed `uses:` reference to a non-builtin action. */ export type ActionReference = | { kind: "remote"; owner: string; repo: string; path: string; ref: string } | { kind: "local"; path: string } | { kind: "docker"; image: string }; const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/; const REPO = /^[A-Za-z0-9._-]{1,100}$/; const SEGMENT = /^[A-Za-z0-9._-]{1,255}$/; const REF = /^[A-Za-z0-9._\/+-]{1,255}$/; export const FULL_SHA = /^[0-9a-f]{40}$/; /** SHA-256 object ids, for repositories converted to the newer object format. */ const FULL_SHA256 = /^[0-9a-f]{64}$/; function safePath(segments: string[]): boolean { return segments.every((segment) => SEGMENT.test(segment) && segment !== "." && segment !== ".."); } export function parseActionReference(raw: string): ActionReference { const uses = raw.trim(); if (uses.startsWith("docker://")) return { kind: "docker", image: uses.slice("docker://".length) }; if (uses.startsWith("./")) { const segments = uses.slice(2).split("/").filter(Boolean); if (segments.length > 0 && !safePath(segments)) throw new Error(`invalid local action path '${uses}'`); return { kind: "local", path: segments.join("/") }; } const at = uses.lastIndexOf("@"); if (at <= 0) throw new Error(`action '${uses}' needs a version, like owner/repo@v1 or owner/repo@`); const ref = uses.slice(at + 1); const [owner = "", repo = "", ...rest] = uses.slice(0, at).split("/"); if (!OWNER.test(owner) || !REPO.test(repo) || repo === "." || repo === "..") { throw new Error(`invalid action reference '${uses}'`); } if (!REF.test(ref) || ref.includes("..") || ref.startsWith("/") || ref.endsWith("/")) { throw new Error(`invalid action version '${ref}' in '${uses}'`); } if (rest.length > 0 && !safePath(rest)) throw new Error(`invalid action path in '${uses}'`); return { kind: "remote", owner, repo: repo.replace(/\.git$/, ""), path: rest.join("/"), ref }; } /** Only a full commit id pins an action; short SHAs are rejected like GitHub does. */ export function isPinned(ref: string): boolean { return FULL_SHA.test(ref) || FULL_SHA256.test(ref); } export function looksLikeShortSha(ref: string): boolean { return /^[0-9a-f]{7,39}$/.test(ref); }