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 | ||
| 11 | import { 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. */ | |
| 14 | export 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). */ | |
| 23 | export 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. */ | |
| 30 | export 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 | */ | |
| 37 | export 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 | */ | |
| 59 | export 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 | */ | |
| 69 | export 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 | */ | |
| 85 | export 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. */ | |
| 97 | export 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 | */ | |
| 109 | export 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 | */ | |
| 129 | export 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.