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 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 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 { 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 | /** | |
| 30 | * The run and job a caller's token was made for, if it is a workflow job's | |
| 31 | * token for `repo` that may write deployments; otherwise why not. | |
| 32 | */ | |
| 33 | export function jobOf(actor: User | null | undefined, repo: RepoPath): { runId: string; jobId: string } | { error: string } { | |
| 34 | const token = actor?.token; | |
| 35 | if (!token?.job?.run_id || !token.job.job_id) { | |
| 36 | return { error: "Only a g1t Actions job's token (G1T_TOKEN) can open a g1t.page build." }; | |
| 37 | } | |
| 38 | const own = `${repo.namespace}/${repo.name}`.toLowerCase(); | |
| 39 | if (!token.repo || token.repo.toLowerCase() !== own) { | |
| 40 | return { error: `This job's token is for ${token.repo ?? "another repository"}, not ${repo.namespace}/${repo.name}.` }; | |
| 41 | } | |
| 42 | if (!tokenAllows(token.scopes, "deployments:write")) { | |
| 43 | return { error: "The job's token needs `deployments: write` in the workflow's or job's `permissions:`." }; | |
| 44 | } | |
| 45 | return { runId: token.job.run_id, jobId: token.job.job_id }; | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * What a run builds. A run for a pull request builds that pull request's | |
| 50 | * preview; a run on the project's production branch (a push, a dispatch | |
| 51 | * or a schedule there) builds production; a run on any other branch | |
| 52 | * builds that branch's preview. A tag deploys nothing: give the branch. | |
| 53 | */ | |
| 54 | export function targetOf(run: Pick<RunFacts, "event" | "ref" | "pull">, productionBranch: string): RunTarget { | |
| 55 | if (run.pull != null && run.event.startsWith("pull_request")) return { kind: "pull", number: run.pull }; | |
| 56 | const ref = run.ref.trim(); | |
| 57 | const branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.startsWith("refs/") ? null : ref; | |
| 58 | if (!branch) { | |
| 59 | return { error: `This run is for ${ref || "no ref"}: g1t.page deploys branches and pull requests. Run the workflow on a branch.` }; | |
| 60 | } | |
| 61 | return branch === productionBranch ? { kind: "production" } : { kind: "branch", branch }; | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Whether a run's build gets the project's build secrets: never for a | |
| 66 | * pull request from a fork, one whose author could not push here, or a | |
| 67 | * run that had to be approved to start. A run on a branch of the | |
| 68 | * repository is trusted: only people who can push make one. | |
| 69 | */ | |
| 70 | export function runTrusted(input: { | |
| 71 | approval: { state: string } | null | undefined; | |
| 72 | pull: { fork?: RepoPath | null } | null; | |
| 73 | /** Whether whoever the pull request is for can push to the repository. */ | |
| 74 | insider: boolean; | |
| 75 | }): boolean { | |
| 76 | if (input.approval) return false; | |
| 77 | if (input.pull) return !input.pull.fork && input.insider; | |
| 78 | return true; | |
| 79 | } | |
| 80 | ||
| 81 | /** The commit to build: the one given (a whole or abbreviated hex id), else the run's. */ | |
| 82 | export function commitOf(given: unknown, run: Pick<RunFacts, "sha">): string | { error: string } { | |
| 83 | if (given == null || given === "") return run.sha; | |
| 84 | if (typeof given !== "string" || !/^[0-9a-f]{7,64}$/i.test(given.trim())) { | |
| 85 | return { error: "`sha` is a commit's id, in hex." }; | |
| 86 | } | |
| 87 | return given.trim().toLowerCase(); | |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * The project to build: the one named, or the repository's one project | |
| 92 | * with Deployments on when none is named. | |
| 93 | */ | |
| 94 | export function projectNamed<P extends { slug: string }>( | |
| 95 | projects: P[], | |
| 96 | enabled: (project: P) => boolean, | |
| 97 | slug: string | null | undefined, | |
| 98 | ): P | { error: string; code: "not_found" | "invalid" | "conflict" } { | |
| 99 | const wanted = typeof slug === "string" ? slug.trim().toLowerCase() : ""; | |
| 100 | if (wanted) { | |
| 101 | return projects.find((p) => p.slug.toLowerCase() === wanted) ?? { error: `This repository has no project ${wanted}.`, code: "not_found" }; | |
| 102 | } | |
| 103 | const on = projects.filter(enabled); | |
| 104 | if (on.length === 1) return on[0]; | |
| 105 | if (on.length === 0) return { error: "No project of this repository has Deployments on.", code: "conflict" }; | |
| 106 | return { error: `Name the project to build: ${on.map((p) => p.slug).join(", ")}.`, code: "invalid" }; | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * What `start_from_workflow` answers, in `snake_case` as the API sends it: | |
| 111 | * the build's id and token for `g1t-runner deploy`, what it is, where it | |
| 112 | * will be served, and what the project gives its build. | |
| 113 | */ | |
| 114 | export type WorkflowBuild = { | |
| 115 | deploy_id: string; | |
| 116 | /** For this build alone, until it finishes: `DEPLOY_TOKEN`. */ | |
| 117 | token: string; | |
| 118 | kind: "production" | "preview"; | |
| 119 | project: string; | |
| 120 | branch: string | null; | |
| 121 | number: number | null; | |
| 122 | commit: string; | |
| 123 | /** The app's address on g1t.page. */ | |
| 124 | url: string; | |
| 125 | /** Whether the build got the project's build secrets. */ | |
| 126 | trusted: boolean; | |
| 127 | /** The project's settings, for a build that names none of its own. */ | |
| 128 | root_dir: string; | |
| 129 | build_command: string | null; | |
| 130 | output_dir: string | null; | |
| 131 | /** The variables builds of this kind get. */ | |
| 132 | build_env: Record<string, string>; | |
| 133 | /** The secrets builds of this kind get; empty for an untrusted run. */ | |
| 134 | build_secrets: Record<string, string>; | |
| 135 | }; |
This file's history is long; its oldest lines are credited to the oldest commit read.