Skip to content
150 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.1/**
2 * Builds opened from a g1t Actions job (`start_from_workflow`): who may
3 * open one, what the run builds, and whether it is trusted with the
4 * project's build secrets. Pure, so it is tested apart from the service.
5 *
6 * The job builds in its own checkout and pays for its own time; the
7 * service only opens the build (its row, its token, the commit's status
8 * and what the project's settings give it) and puts what the job sends.
9 */
10
11import { isManagedWorkflow, tokenAllows, type RepoPath, type User } from "@g1t/contracts";
12
13/** The run a job token was made for, as far as these rules read it. */
14export type RunFacts = {
15 event: string;
16 ref: string;
17 sha: string;
18 pull: number | null;
19 status: string;
20};
21
22/** What a run deploys: production, a branch's preview, or a pull request's (its branch looked up). */
23export type RunTarget =
24 | { kind: "production" }
25 | { kind: "branch"; branch: string }
26 | { kind: "pull"; number: number }
27 | { error: string };
28
29/** Why a token that cannot write deployments is refused, where nothing lets it through. */
30export const NEEDS_WRITE = "The job's token needs `deployments: write` in the workflow's or job's `permissions:`.";
31
32/**
33 * The run and job a caller's token was made for, if it is a workflow job's
34 * token for `repo`; otherwise why not. `writes` says whether it may write
35 * deployments: one that may not is let through only by `readOnlyAllowed`.
36 */
37export function jobOf(
38 actor: User | null | undefined,
39 repo: RepoPath,
40): { runId: string; jobId: string; writes: boolean } | { error: string } {
41 const token = actor?.token;
42 if (!token?.job?.run_id || !token.job.job_id) {
43 return { error: "Only a g1t Actions job's token (G1T_TOKEN) can open a g1t.page build." };
44 }
45 const own = `${repo.namespace}/${repo.name}`.toLowerCase();
46 if (!token.repo || token.repo.toLowerCase() !== own) {
47 return { error: `This job's token is for ${token.repo ?? "another repository"}, not ${repo.namespace}/${repo.name}.` };
48 }
49 return { runId: token.job.run_id, jobId: token.job.job_id, writes: tokenAllows(token.scopes, "deployments:write") };
50}
51
52/**
53 * Whether a job whose token may not write deployments may still open a
54 * build: only a preview, from the deploy workflow g1t manages for the
55 * project (a run of `managed/…`). A pull request from a fork runs with a
56 * read-only token, as on GitHub, and still gets the preview g1t.page has
57 * always built for it, without the project's build secrets.
58 */
59export function readOnlyAllowed(run: { path?: string | null }, target: RunTarget): boolean {
60 return isManagedWorkflow(run.path ?? "") && "kind" in target && target.kind !== "production";
61}
62
63/**
64 * What a run builds. A run for a pull request builds that pull request's
65 * preview; a run on the project's production branch (a push, a dispatch
66 * or a schedule there) builds production; a run on any other branch
67 * builds that branch's preview. A tag deploys nothing: give the branch.
68 */
69export function targetOf(run: Pick<RunFacts, "event" | "ref" | "pull">, productionBranch: string): RunTarget {
70 if (run.pull != null && run.event.startsWith("pull_request")) return { kind: "pull", number: run.pull };
71 const ref = run.ref.trim();
72 const branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.startsWith("refs/") ? null : ref;
73 if (!branch) {
74 return { error: `This run is for ${ref || "no ref"}: g1t.page deploys branches and pull requests. Run the workflow on a branch.` };
75 }
76 return branch === productionBranch ? { kind: "production" } : { kind: "branch", branch };
77}
78
79/**
80 * Whether a run's build gets the project's build secrets: never for a
81 * pull request from a fork, one whose author could not push here, or a
82 * run that had to be approved to start. A run on a branch of the
83 * repository is trusted: only people who can push make one.
84 */
85export function runTrusted(input: {
86 approval: { state: string } | null | undefined;
87 pull: { fork?: RepoPath | null } | null;
88 /** Whether whoever the pull request is for can push to the repository. */
89 insider: boolean;
90}): boolean {
91 if (input.approval) return false;
92 if (input.pull) return !input.pull.fork && input.insider;
93 return true;
94}
95
96/** The commit to build: the one given (a whole or abbreviated hex id), else the run's. */
97export function commitOf(given: unknown, run: Pick<RunFacts, "sha">): string | { error: string } {
98 if (given == null || given === "") return run.sha;
99 if (typeof given !== "string" || !/^[0-9a-f]{7,64}$/i.test(given.trim())) {
100 return { error: "`sha` is a commit's id, in hex." };
101 }
102 return given.trim().toLowerCase();
103}
104
105/**
106 * The project to build: the one named, or the repository's one project
107 * with Deployments on when none is named.
108 */
109export function projectNamed<P extends { slug: string }>(
110 projects: P[],
111 enabled: (project: P) => boolean,
112 slug: string | null | undefined,
113): P | { error: string; code: "not_found" | "invalid" | "conflict" } {
114 const wanted = typeof slug === "string" ? slug.trim().toLowerCase() : "";
115 if (wanted) {
116 return projects.find((p) => p.slug.toLowerCase() === wanted) ?? { error: `This repository has no project ${wanted}.`, code: "not_found" };
117 }
118 const on = projects.filter(enabled);
119 if (on.length === 1) return on[0];
120 if (on.length === 0) return { error: "No project of this repository has Deployments on.", code: "conflict" };
121 return { error: `Name the project to build: ${on.map((p) => p.slug).join(", ")}.`, code: "invalid" };
122}
123
124/**
125 * What `start_from_workflow` answers, in `snake_case` as the API sends it:
126 * the build's id and token for `g1t-runner deploy`, what it is, where it
127 * will be served, and what the project gives its build.
128 */
129export type WorkflowBuild = {
130 deploy_id: string;
131 /** For this build alone, until it finishes: `DEPLOY_TOKEN`. */
132 token: string;
133 kind: "production" | "preview";
134 project: string;
135 branch: string | null;
136 number: number | null;
137 commit: string;
138 /** The app's address on g1t.page. */
139 url: string;
140 /** Whether the build got the project's build secrets. */
141 trusted: boolean;
142 /** The project's settings, for a build that names none of its own. */
143 root_dir: string;
144 build_command: string | null;
145 output_dir: string | null;
146 /** The variables builds of this kind get. */
147 build_env: Record<string, string>;
148 /** The secrets builds of this kind get; empty for an untrusted run. */
149 build_secrets: Record<string, string>;
150};

This file's history is long; its oldest lines are credited to the oldest commit read.