/** 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@<commit sha>`);
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 };
}