Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
319 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.

Project settings say who created the project, by name1import type { ServiceBinding } from "./clients";
2import type { User, Viewer } from "./identity";
3import type { RepoPath } from "./repos";
4import type { Result } from "./result";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar5import type { BumpPackage, BumpRegistry, VersionUpdatesState } from "./updates";
6
7export * from "./updates";
Project settings say who created the project, by name8
9/**
10 * The security service: secrets found in pushes and in history, vulnerable
11 * dependencies, and the upgrade issues g1t opens for them. Members of a
12 * workspace see its findings; nobody else does. Mirrors
13 * `g1t_contracts::security`.
14 */
15
16/**
17 * Where a secret stands. `open`: in history, to be rotated. `blocked`: a
18 * push carrying it was refused, so it never landed. `allowed`: someone said
19 * it is not a real secret, and pushes carrying it go through. `resolved`:
20 * rotated or removed.
21 */
22export type SecretStatus = "open" | "blocked" | "allowed" | "resolved";
23
24/**
25 * Where an alert stands, as the Security page's filters and the API put it.
26 * `open`: needs someone. `dismissed`: someone said why it can stay.
27 * `fixed`: a secret revoked, or a dependency no longer vulnerable.
28 */
29export type AlertState = "open" | "dismissed" | "fixed";
30
31/** Why a person dismissed an alert. */
32export type DismissReason =
33 | "false_positive"
34 | "used_in_tests"
35 | "revoked"
36 | "wont_fix"
37 | "fix_started"
38 | "no_bandwidth"
39 | "tolerable_risk"
40 | "inaccurate"
41 | "not_used";
42
43/** The reasons a secret alert can be dismissed with, as people read them. */
44export const SECRET_DISMISS_REASONS: { reason: DismissReason; label: string; about: string }[] = [
45 { reason: "false_positive", label: "False positive", about: "It is not a secret." },
46 { reason: "used_in_tests", label: "Used in tests", about: "A value made for tests or examples." },
47 { reason: "revoked", label: "Revoked", about: "It was real and has been revoked or rotated." },
48 { reason: "wont_fix", label: "Won't fix", about: "It is real, and accepted as it is." },
49];
50
51/** The reasons a dependency alert can be dismissed with, as people read them. */
52export const DEPENDENCY_DISMISS_REASONS: { reason: DismissReason; label: string; about: string }[] = [
53 { reason: "fix_started", label: "A fix has already been started", about: "Someone is upgrading it." },
54 { reason: "no_bandwidth", label: "No bandwidth to fix this", about: "Nobody can get to it now." },
55 { reason: "tolerable_risk", label: "Risk is tolerable to this project", about: "It does not matter for how this project uses it." },
56 { reason: "inaccurate", label: "This alert is inaccurate or incorrect", about: "The advisory is wrong about this package or version." },
57 { reason: "not_used", label: "Vulnerable code is not actually used", about: "The vulnerable code is never called." },
58];
59
60/** A reason's label. */
61export function dismissLabel(reason: DismissReason): string {
62 return [...SECRET_DISMISS_REASONS, ...DEPENDENCY_DISMISS_REASONS].find((r) => r.reason === reason)?.label ?? reason;
63}
64
65export type SecretFinding = {
66 id: string;
67 repoId: string;
68 /** `aws_access_key`, `github_token`, … */
69 kind: string;
70 /** "an AWS access key". */
71 label: string;
72 path: string;
73 line: number;
74 commit: string;
75 /** Enough of the secret to recognise it; the secret itself is never kept. */
76 preview: string;
77 status: SecretStatus;
78 source: "push" | "history";
79 foundBy: string | null;
80 /** RFC 3339. */
81 foundAt: string;
82 /** Who dismissed it (allowed or resolved it). */
83 decidedBy: string | null;
84 /** The comment given when it was dismissed. */
85 reason: string | null;
86 decidedAt: string | null;
87 /** Why it was dismissed; absent on open alerts and older decisions. */
88 dismissedReason?: DismissReason | null;
89 /**
90 * Why the value looks made for tests or documentation, when it does. Such
91 * an alert never stops a push and is never counted as critical.
92 */
93 testValue?: string | null;
94 state: AlertState;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar95 /** What its issuer said when last asked: active, inactive, unknown or unsupported. */
96 validity?: "active" | "inactive" | "unknown" | "unsupported" | null;
97 validityCheckedAt?: string | null;
98 /** How it got past push protection, when someone bypassed it. */
99 bypass?: {
100 reason: "false_positive" | "used_in_tests" | "will_fix_later";
101 comment: string | null;
102 by: string;
103 at: string;
104 approvedBy: string | null;
105 } | null;
106 /** The custom pattern that found it, for a `custom_pattern` finding. */
107 patternId?: string | null;
108 patternName?: string | null;
109 /** How many places it was found in. */
110 locations?: number;
Project settings say who created the project, by name111};
112
113export type Severity = "critical" | "high" | "medium" | "low" | "unknown";
114
115export const SEVERITIES: Severity[] = ["critical", "high", "medium", "low", "unknown"];
116
117export type Vulnerability = {
118 id: string;
119 repoId: string;
120 /** `npm`, `crates.io`, `Go`, `PyPI`. */
121 ecosystem: string;
122 package: string;
123 version: string;
124 /** The lockfile that resolves it. */
125 manifest: string;
126 /** Its GHSA id when it has one. */
127 advisory: string;
128 osvId: string;
129 summary: string;
130 severity: Severity;
131 fixedVersion: string | null;
132 status: "open" | "fixed" | "dismissed";
133 /** The issue opened for g1t, when upgrading needs code changes. */
134 issue: number | null;
135 foundAt: string;
136 fixedAt: string | null;
137 state: AlertState;
138 dismissedBy?: string | null;
139 dismissedReason?: DismissReason | null;
140 dismissedComment?: string | null;
141 dismissedAt?: string | null;
142 /** The security update for its package, if g1t has started one. */
143 update?: SecurityUpdate | null;
144};
145
146/**
147 * Where a security update stands. `requested`: a sandbox is making the
148 * change. `open`: its pull request is going through the required checks.
149 * `superseded`: a newer update replaced it, or the package is no longer
Security and version updates open no issues on their own: an update whose version cannot be raised fails with why on its alert and is tried again a day later, and one whose required checks fail stays open for a person to decide on; an agent takes one only when a person asks with Fix with g1t. That issue now names the lockfiles the package is locked in and the commands that show it is done, which were left empty ("Locked in: .") because the lockfiles were never passed in.150 * vulnerable. `needs_code`: an earlier update g1t opened an issue for; new
151 * updates no longer reach it. `failed`: see `error`.
Project settings say who created the project, by name152 */
153export type UpdateState = "requested" | "open" | "merged" | "closed" | "superseded" | "needs_code" | "failed";
154
155/** The pull request g1t opens itself to upgrade one vulnerable package. */
156export type SecurityUpdate = {
157 state: UpdateState;
158 /** The version it upgrades to. */
159 target: string;
160 /** `g1t/security/<package>-<version>`. */
161 branch: string | null;
162 pull: number | null;
163 issue: number | null;
164 error: string | null;
165 updatedAt: string;
166};
167
168/** Secret alerts by where they stand. */
169export type SecretCounts = {
170 /** In the history and looking real: rotate these. */
171 open: number;
172 /** Stopped at a push, so never landed, and looking real. */
173 blocked: number;
174 /** Open or blocked, but likely test values. */
175 testValues: number;
176 dismissed: number;
177 fixed: number;
178};
179
180/** One thing that happened to an alert. */
181export type AlertActivity = {
182 id: string;
183 alertId: string;
184 /**
185 * `dismissed`, `reopened`, `update_requested`, `update_opened`,
186 * `update_merged`, `update_closed`, `update_superseded`,
187 * `update_needs_code` or `update_failed`.
188 */
189 action: string;
190 /** A person's username, or `g1t`. */
191 actor: string | null;
192 reason: DismissReason | null;
193 comment: string | null;
194 /** The pull request or issue it concerns. */
195 number: number | null;
196 at: string;
197};
198
199export type SeverityCounts = Record<Severity, number>;
200
201export type ScanState = {
202 /** `pending`, `running`, `done`, or `stopped` at the workspace's limit. */
203 history: "pending" | "running" | "done" | "stopped";
204 commitsScanned: number;
205 historyFinishedAt: string | null;
206 dependenciesScannedAt: string | null;
207 dependenciesError: string | null;
208 lockfiles: string[];
209};
210
211export type SecurityOverview = {
212 repoId: string;
213 /**
214 * Open alerts by severity: vulnerabilities open and not dismissed, and
215 * secrets in the history that look real, as critical. Blocked secrets
216 * and likely test values are not counted.
217 */
218 counts: SeverityCounts;
219 secretCounts: SecretCounts;
220 secrets: SecretFinding[];
221 vulnerabilities: Vulnerability[];
222 /** What happened to the alerts, newest first. */
223 activity: AlertActivity[];
224 scan: ScanState;
225 /** Security updates: whether g1t opens a pull request for each vulnerable dependency with a fix. */
226 upkeep: boolean;
227 versionUpdates: VersionUpdatesState;
228};
229
230/** The alert `dismiss` or `reopen` changed, as it is now. */
231export type AlertChange = {
232 secret: SecretFinding | null;
233 vulnerability: Vulnerability | null;
234};
235
236export type RepoSecurity = {
237 repoId: string;
238 name: string;
239 counts: SeverityCounts;
240 secrets: number;
241 vulnerabilities: number;
242 upkeep: boolean;
243 dependenciesScannedAt: string | null;
244};
245
246export interface SecurityApi {
247 overview(repo: RepoPath, viewer: Viewer): Promise<Result<SecurityOverview>>;
248 /**
249 * Dismisses an alert (a secret takes Admin, a dependency Write) with a
250 * reason and an optional comment.
251 */
252 dismiss(actor: User, repo: RepoPath, id: string, reason: DismissReason, comment: string): Promise<Result<AlertChange>>;
253 /** Opens a dismissed alert again. */
254 reopen(actor: User, repo: RepoPath, id: string): Promise<Result<AlertChange>>;
255 rescan(actor: User, repo: RepoPath): Promise<Result<ScanState>>;
256 setUpkeep(actor: User, repo: RepoPath, enabled: boolean): Promise<Result<boolean>>;
257 workspace(workspace: string, viewer: Viewer): Promise<Result<RepoSecurity[]>>;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar258 /** Checks one `updates` entry of the dependency update file for new versions now. Write and up. */
259 checkUpdates(actor: User, repo: RepoPath, entry: string): Promise<Result<VersionUpdatesState>>;
Project settings say who created the project, by name260}
261
262export function securityClient(service: ServiceBinding): SecurityApi {
263 const call = async <T>(method: string, args: object): Promise<T> => {
264 const response = await service.fetch(`https://service/rpc/${method}`, {
265 method: "POST",
266 headers: { "content-type": "application/json" },
267 body: JSON.stringify(args),
268 });
269 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
270 return (await response.json()) as T;
271 };
272 return {
273 overview: (repo, viewer) => call("overview", { repo, viewer }),
274 dismiss: (actor, repo, id, reason, comment) => call("dismiss", { actor, repo, id, reason, comment }),
275 reopen: (actor, repo, id) => call("reopen", { actor, repo, id }),
276 rescan: (actor, repo) => call("rescan", { actor, repo }),
277 setUpkeep: (actor, repo, enabled) => call("set_upkeep", { actor, repo, enabled }),
278 workspace: (workspace, viewer) => call("workspace", { workspace, viewer }),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar279 checkUpdates: (actor, repo, entry) => call("check_updates", { actor, repo, entry }),
Project settings say who created the project, by name280 };
281}
282
283/**
284 * The runner's `bump`: makes a security update in a sandbox. It clones the
285 * default branch, raises `package` to `version` in each lockfile with the
286 * ecosystem's own tool, commits that as g1t and pushes it to `branch`
287 * (`g1t/security/…`). The push tells the security service to open the pull
288 * request. Mirrors `g1t_contracts::security::BumpArgs`.
289 */
290export type BumpArgs = {
291 repo: RepoPath;
292 /** OSV's name for the ecosystem: `npm`, `crates.io`, `Go` or `PyPI`. */
293 ecosystem: string;
294 package: string;
295 version: string;
296 /** The lockfiles that resolve a vulnerable version, from the root. */
297 lockfiles: string[];
298 branch: string;
299 /** The commit's message. */
300 message: string;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar301 /** `version` for a version update, whose branch is any but the default one. */
302 kind?: "version";
303 /** Several packages raised in one commit, for a grouped update. */
304 packages?: BumpPackage[];
305 /** `increase` (default), `increase-if-necessary`, `widen` or `lockfile-only`. */
306 strategy?: string;
307 /** Replace the branch if it is there already: a rebase or a recreate. */
308 force?: boolean;
309 /** Private registries the tools may read, with their credentials. */
310 registries?: BumpRegistry[];
311 /**
312 * The branch to start from, which its pull request merges into
313 * (`target-branch`): the default branch when absent.
314 */
315 base?: string;
Project settings say who created the project, by name316};
317
318/** The prefix every security update's branch starts with. */
319export const UPDATE_BRANCH_PREFIX = "g1t/security/";

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