Skip to content
989 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.

API reference examples use projects1/**
2 * The projects service: what a workspace builds and runs.
3 *
4 * A project has one source, where its code lives: today a repository hosted
5 * on g1t and a root directory in it. Everything about running it
6 * (deployments, environments, domains, secrets and variables) hangs off the
7 * project; other services key their data by its id. Every repository gets
8 * a project of its own name: when it is created (from `repo.created`), and
9 * for repositories made before projects existed, the first time their
10 * workspace's projects are asked for.
11 *
12 * Reached through service bindings: `POST /rpc/<method>`.
13 */
14
Project dependencies: addresses, preview stacks, Affects, and agents who know15
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look16import { NEEDS, repoRef } from "./access";
Fast pages, required checks on the branch, self-hosted runners, honest incidents17import { effectiveDescription, ownDescription } from "./description";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9718import {
19 type KindFacts,
20 type RootFiles,
21 MANIFESTS,
22 detectKind,
23 detectedKind,
24 goFilesToRead,
25 kindSetting,
26 neverRuns,
27 nextSetting,
28 resolveKind,
29 runsSetting,
30} from "./kind";
31import { cleanLinks, cleanUrl, projectLinks } from "./links";
Projects: when each was last pushed, and how active it is lately32import { ACTIVE, decayed, raised } from "./activity";
Projects: pinned and recent projects, per person per workspace33import { MAX_PINS, MAX_RECENT, MAX_VISITS, placePin, recentAfterPins, reorderPins } from "./pins";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains34import { renameStatements } from "./rename";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look35import { moveStatements, slugOf, strandedQuery } from "./transfer";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains36
API reference examples use projects37import {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38 can,
39 currentMovedPath,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains40 currentWorkspaceSlug,
API reference examples use projects41 fail,
42 identityClient,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains43 staleSlugs,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44 needs,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member45 isProtectedWorkspace,
API reference examples use projects46 newId,
47 ok,
Fast pages, required checks on the branch, self-hosted runners, honest incidents48 openD1,
A project is an app or a library: libraries show their package and how to ship a release, not production49 packagesClient,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50 permission,
51 repoMove,
API reference examples use projects52 reposClient,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53 staleMovedPaths,
A project is an app or a library: libraries show their package and how to ship a release, not production54 type Ecosystem,
API reference examples use projects55 type G1tEvent,
56 type NewProject,
57 type Project,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9758 type ProjectChanges,
59 type ProjectKind,
A project is an app or a library: libraries show their package and how to ship a release, not production60 type ProjectEcosystem,
Projects: pinned and recent projects, per person per workspace61 type ProjectShortcuts,
API reference examples use projects62 type Repo,
63 type Result,
64 type ServiceBinding,
65 type User,
66 type Viewer,
67} from "@g1t/contracts";
68
69type Env = {
70 DB: D1Database;
71 REPOS: ServiceBinding;
72 IDENTITY: ServiceBinding;
A project is an app or a library: libraries show their package and how to ship a release, not production73 PACKAGES: ServiceBinding;
74 DEPLOYMENTS: ServiceBinding;
API reference examples use projects75};
76
77type Row = {
78 id: string;
79 workspace: string;
80 slug: string;
81 name: string;
Fast pages, required checks on the branch, self-hosted runners, honest incidents82 /** The project's own description; null while it follows its repository's. */
API reference examples use projects83 description: string | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents84 /** Its repository's description, kept from repos. */
85 repo_description?: string | null;
API reference examples use projects86 source_kind: string;
87 repo_id: string;
88 repo_namespace: string;
89 repo_name: string;
90 repo_private: number;
91 default_branch: string;
92 root_dir: string;
93 is_primary: number;
94 created_by: string;
95 created_at: string;
96 updated_at: string;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 /** Set while its repository is deleted; the project is hidden until it is restored. */
98 repo_deleted_at: string | null;
99 /** When its repository was archived; null while it is not. */
100 repo_archived_at?: string | null;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97101 /** What a person set it to be and where it runs; null left to detection (migrations/0010_kind_and_links.sql). */
102 kind_setting?: string | null;
103 runs_setting?: string | null;
104 production_url?: string | null;
105 /** Its own homepage; null follows `repo_website`. */
106 homepage?: string | null;
107 repo_website?: string | null;
108 docs_url?: string | null;
109 /** Its other links, as JSON (src/links.ts). */
110 links?: string | null;
A project is an app or a library: libraries show their package and how to ship a release, not production111 detected_kind?: string | null;
112 detected_detail?: string | null;
113 detected_ecosystem?: string | null;
114 /** The default branch's commit its files were read at; null until they have been. */
115 detected_commit?: string | null;
116 linked_package?: string | null;
117 deployments_on?: number | null;
Projects: when each was last pushed, and how active it is lately118 /** When its repository was last pushed to (migrations/0009_activity.sql). */
119 pushed_at?: string | null;
120 /** How active it is lately, as of `active_at` (src/activity.ts). */
121 activity?: number | null;
122 active_at?: string | null;
API reference examples use projects123};
124
A project is an app or a library: libraries show their package and how to ship a release, not production125/** How a package its repository publishes is named in why a project is a library. */
126const ECOSYSTEM_NAME: Record<Ecosystem, string> = {
127 container: "container image",
128 npm: "npm package",
129 composer: "Composer package",
130 cargo: "crate",
131 go: "Go module",
Projects: a library publishing a Maven, NuGet or RubyGems package says so132 maven: "Maven package",
133 nuget: "NuGet package",
134 rubygems: "gem",
A project is an app or a library: libraries show their package and how to ship a release, not production135};
136
137/** How many projects one listing reads the files of, in the background, before it has. */
138const DETECT_PER_LIST = 10;
139
API reference examples use projects140const now = () => new Date().toISOString();
141
142function toProject(row: Row): Project {
Fast pages, required checks on the branch, self-hosted runners, honest incidents143 const { description, inherited } = effectiveDescription(row);
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97144 const setting = { kind: kindSetting(row.kind_setting), runs: runsSetting(row.runs_setting) };
145 const facts: Omit<KindFacts, "setting"> = {
A project is an app or a library: libraries show their package and how to ship a release, not production146 deploymentsOn: row.deployments_on == null ? null : !!row.deployments_on,
147 linkedPackage: row.linked_package ?? null,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97148 detected: row.detected_commit == null ? null : { kind: kindSetting(row.detected_kind), detail: row.detected_detail ?? "" },
A project is an app or a library: libraries show their package and how to ship a release, not production149 };
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97150 const { kind, reason, runs } = resolveKind({ setting, ...facts });
API reference examples use projects151 return {
152 id: row.id,
153 workspace: row.workspace,
154 slug: row.slug,
155 name: row.name,
Fast pages, required checks on the branch, self-hosted runners, honest incidents156 description,
157 descriptionInherited: inherited,
API reference examples use projects158 source: {
159 kind: "hosted",
160 repoId: row.repo_id,
161 repo: { namespace: row.repo_namespace, name: row.repo_name },
162 rootDir: row.root_dir,
163 defaultBranch: row.default_branch,
164 },
165 private: !!row.repo_private,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look166 archived: !!row.repo_archived_at,
API reference examples use projects167 primary: !!row.is_primary,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97168 setting,
A project is an app or a library: libraries show their package and how to ship a release, not production169 kind,
170 kindReason: reason,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97171 runs,
172 productionUrl: row.production_url ?? null,
173 detected: detectedKind(facts),
A project is an app or a library: libraries show their package and how to ship a release, not production174 ecosystem: (row.detected_ecosystem as ProjectEcosystem | null) ?? null,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97175 links: projectLinks(row),
API reference examples use projects176 createdBy: row.created_by,
177 createdAt: row.created_at,
178 updatedAt: row.updated_at,
Projects: when each was last pushed, and how active it is lately179 pushedAt: row.pushed_at ?? null,
180 activity: decayed(row.activity ?? 0, row.active_at ?? null, now()),
API reference examples use projects181 };
182}
183
184function isMember(viewer: Viewer, workspace: string): boolean {
185 return !!viewer?.workspaces?.some((m) => m.slug === workspace.toLowerCase());
186}
187
188class Projects {
A project is an app or a library: libraries show their package and how to ship a release, not production189 /** `defer` runs work after the answer is sent: the request's waitUntil. */
190 constructor(
191 private readonly env: Env,
192 private readonly defer: (work: Promise<unknown>) => void = () => {},
193 ) {}
API reference examples use projects194
195 private get db() {
196 return this.env.DB;
197 }
198
199 private async workspaceActor(slug: string): Promise<User | null> {
200 const workspace = await identityClient(this.env.IDENTITY).getWorkspace(slug);
201 if (!workspace) return null;
202 return {
203 id: workspace.id,
204 username: workspace.slug,
205 kind: "workspace",
206 verified: true,
207 workspaces: [{ slug: workspace.slug, role: "member" }],
208 };
209 }
210
211 /** A free slug for `wanted` in the workspace. */
212 private async freeSlug(workspace: string, wanted: string): Promise<string> {
213 const base = slugOf(wanted) || "project";
214 for (let n = 1; n < 100; n++) {
215 const slug = n === 1 ? base : `${base}-${n}`;
216 const taken = await this.db
217 .prepare("SELECT 1 FROM projects WHERE workspace = ? AND slug = ?")
218 .bind(workspace, slug)
219 .first();
220 if (!taken) return slug;
221 }
222 return `${base}-${crypto.randomUUID().slice(0, 6)}`;
223 }
224
225 /** Gives a repository its own project, unless it has one. */
226 private async ensureFor(repo: Repo, createdBy: string): Promise<void> {
227 if (repo.forkOf) return;
228 const existing = await this.db.prepare("SELECT id FROM projects WHERE repo_id = ?").bind(repo.id).first();
229 if (existing) {
230 await this.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look231 .prepare(
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97232 "UPDATE projects SET repo_private = ?, default_branch = ?, repo_name = ?, repo_archived_at = ?, repo_description = ?, repo_website = ? WHERE repo_id = ?",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look233 )
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97234 .bind(repo.isPrivate ? 1 : 0, repo.defaultBranch, repo.name, repo.archivedAt ?? null, repo.description ?? null, repo.website ?? null, repo.id)
API reference examples use projects235 .run();
236 return;
237 }
238 const at = now();
239 await this.db
240 .prepare(
Fast pages, required checks on the branch, self-hosted runners, honest incidents241 // No description of its own: it shows the repository's, as that changes.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97242 `INSERT INTO projects (id, workspace, slug, name, description, repo_description, repo_website, repo_id, repo_namespace, repo_name, repo_private,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look243 default_branch, root_dir, is_primary, created_by, created_at, updated_at, repo_archived_at)
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97244 VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, '', 1, ?, ?, ?, ?)
API reference examples use projects245 ON CONFLICT (workspace, slug) DO NOTHING`,
246 )
247 .bind(
248 newId("prj"),
249 repo.namespace.toLowerCase(),
250 await this.freeSlug(repo.namespace.toLowerCase(), repo.name),
251 repo.name,
Fast pages, required checks on the branch, self-hosted runners, honest incidents252 repo.description ?? null,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97253 repo.website ?? null,
API reference examples use projects254 repo.id,
255 repo.namespace,
256 repo.name,
257 repo.isPrivate ? 1 : 0,
258 repo.defaultBranch,
259 createdBy,
260 at,
261 at,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look262 repo.archivedAt ?? null,
API reference examples use projects263 )
264 .run();
265 }
266
267 /** Projects for a workspace's repositories made before projects existed. Once. */
268 private async backfill(workspace: string): Promise<void> {
269 const done = await this.db.prepare("SELECT 1 FROM backfilled WHERE workspace = ?").bind(workspace).first();
270 if (done) return;
271 const actor = await this.workspaceActor(workspace);
272 if (!actor) return;
273 const list = await reposClient(this.env.REPOS).list(actor, { namespace: workspace });
274 for (const repo of list) {
275 if (repo.namespace.toLowerCase() === workspace) await this.ensureFor(repo, "g1t");
276 }
277 await this.db.prepare("INSERT OR REPLACE INTO backfilled (workspace, at) VALUES (?, ?)").bind(workspace, now()).run();
278 }
279
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look280 /**
281 * Whether the viewer can read the project's repository. The workspace's
282 * own token sees all its projects, also one left building from a
283 * repository that moved to another workspace.
284 */
API reference examples use projects285 private visible(row: Row, viewer: Viewer): boolean {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look286 if (viewer?.kind === "workspace" && isMember(viewer, row.workspace)) return true;
287 return permission(viewer, repoRef(row)) != null;
288 }
289
290 /**
291 * Whether the actor may change the project: not found when they cannot
292 * read its repository, refused when their role there is too low.
293 */
294 private changeable(row: Row, actor: User, capability: (typeof NEEDS)[keyof typeof NEEDS]): Result<true> {
295 if (!this.visible(row, actor)) return fail("not_found", "There is no such project.");
296 if (!can(actor, repoRef(row), capability)) return fail("forbidden", needs(capability));
297 return ok(true);
API reference examples use projects298 }
299
300 async list(a: { workspace: string; viewer: Viewer }): Promise<Result<Project[]>> {
301 const workspace = a.workspace.toLowerCase();
302 await this.backfill(workspace);
303 const rows = await this.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304 .prepare("SELECT * FROM projects WHERE workspace = ? AND repo_deleted_at IS NULL ORDER BY name COLLATE NOCASE")
API reference examples use projects305 .bind(workspace)
306 .all<Row>();
A project is an app or a library: libraries show their package and how to ship a release, not production307 // Projects whose files have not been read yet are read after this answer,
308 // a few at a time; until then they show as apps, as before.
309 const unread = rows.results.filter((row) => row.detected_commit == null).slice(0, DETECT_PER_LIST);
310 if (unread.length) this.defer(Promise.all(unread.map((row) => this.detect(row).catch((error) => console.warn("detect", row.slug, error)))));
API reference examples use projects311 return ok(rows.results.filter((row) => this.visible(row, a.viewer)).map(toProject));
312 }
313
314 async get(a: { workspace: string; slug: string; viewer: Viewer }): Promise<Result<Project>> {
315 const workspace = a.workspace.toLowerCase();
316 await this.backfill(workspace);
317 const row = await this.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look318 .prepare("SELECT * FROM projects WHERE workspace = ? AND slug = ? AND repo_deleted_at IS NULL")
API reference examples use projects319 .bind(workspace, a.slug.toLowerCase())
320 .first<Row>();
321 if (!row || !this.visible(row, a.viewer)) return fail("not_found", "There is no such project.");
A project is an app or a library: libraries show their package and how to ship a release, not production322 if (row.detected_commit == null) {
323 // Once per project: what its files say, read now so its first page is right.
324 const read = await this.detect(row).catch((error) => (console.warn("detect", row.slug, error), null));
325 if (read) return ok(toProject(read));
326 }
API reference examples use projects327 return ok(toProject(row));
328 }
329
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member330 /** How many projects a workspace shows: what deleting it would take with it. */
331 async count(a: { workspace: string }): Promise<number> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look332 const row = await this.db
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member333 .prepare("SELECT count(*) AS n FROM projects WHERE workspace = ? AND repo_deleted_at IS NULL")
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look334 .bind(a.workspace.toLowerCase())
335 .first<{ n: number }>();
336 return row?.n ?? 0;
337 }
338
339 /** The repository as it is now, read as its workspace; null when it is gone or deleted. */
340 private async repoById(repoId: string): Promise<Repo | null> {
API reference examples use projects341 const path = await this.env.REPOS.fetch("https://repos/rpc/path_by_id", {
342 method: "POST",
343 headers: { "content-type": "application/json" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look344 body: JSON.stringify({ id: repoId }),
API reference examples use projects345 });
346 const repoPath = path.ok ? ((await path.json()) as { namespace: string; name: string } | null) : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look347 if (!repoPath) return null;
API reference examples use projects348 const actor = await this.workspaceActor(repoPath.namespace);
349 const repo = actor ? await reposClient(this.env.REPOS).get(repoPath, actor) : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look350 return repo?.ok ? repo.value : null;
351 }
352
353 async byRepo(a: { repoId: string }): Promise<Project[]> {
354 const rows = await this.db
355 .prepare("SELECT * FROM projects WHERE repo_id = ? ORDER BY is_primary DESC, created_at")
356 .bind(a.repoId)
357 .all<Row>();
358 // A deleted repository's projects are hidden until it is restored.
359 if (rows.results.length > 0) return rows.results.filter((row) => !row.repo_deleted_at).map(toProject);
360 // A repository from before projects: give it its own now.
361 const repo = await this.repoById(a.repoId);
362 if (!repo) return [];
363 await this.ensureFor(repo, "g1t");
364 const again = await this.db
365 .prepare("SELECT * FROM projects WHERE repo_id = ? AND repo_deleted_at IS NULL")
366 .bind(a.repoId)
367 .all<Row>();
API reference examples use projects368 return again.results.map(toProject);
369 }
370
371 async create(a: { actor: User; workspace: string; input: NewProject }): Promise<Result<Project>> {
372 const workspace = a.workspace.toLowerCase();
373 if (!isMember(a.actor, workspace)) return fail("forbidden", "Only members can add projects to a workspace.");
374 if (a.input.repo.namespace.toLowerCase() !== workspace) {
375 return fail("invalid", "A project builds from one of its own workspace's repositories.");
376 }
377 const repo = await reposClient(this.env.REPOS).get(a.input.repo, a.actor);
378 if (!repo.ok) return repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look379 if (!can(a.actor, repo.value, NEEDS.create)) return fail("forbidden", needs(NEEDS.create));
API reference examples use projects380 if (repo.value.forkOf) return fail("invalid", "A pull request's working copy cannot be a project's source.");
381 const name = a.input.name.trim();
382 if (!name || name.length > 100) return fail("invalid", "A project's name is 1 to 100 characters.");
383 const rootDir = (a.input.rootDir ?? "").trim().replace(/^\/+|\/+$/g, "");
384 if (rootDir.split("/").some((part) => part === "..")) return fail("invalid", "The root directory is inside the repository.");
385 const slug = slugOf(name);
386 if (!slug) return fail("invalid", "Give the project a name with letters or digits.");
387 const taken = await this.db.prepare("SELECT 1 FROM projects WHERE workspace = ? AND slug = ?").bind(workspace, slug).first();
388 if (taken) return fail("conflict", `${workspace} already has a project called ${slug}.`);
389 const primary = !(await this.db.prepare("SELECT 1 FROM projects WHERE repo_id = ?").bind(repo.value.id).first());
390 const at = now();
391 const id = newId("prj");
392 await this.db
393 .prepare(
Fast pages, required checks on the branch, self-hosted runners, honest incidents394 `INSERT INTO projects (id, workspace, slug, name, description, repo_description, repo_id, repo_namespace, repo_name, repo_private,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look395 default_branch, root_dir, is_primary, created_by, created_at, updated_at, repo_archived_at)
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
API reference examples use projects397 )
398 .bind(
399 id,
400 workspace,
401 slug,
402 name,
Fast pages, required checks on the branch, self-hosted runners, honest incidents403 ownDescription(null, a.input.description ?? null),
404 repo.value.description ?? null,
API reference examples use projects405 repo.value.id,
406 repo.value.namespace,
407 repo.value.name,
408 repo.value.isPrivate ? 1 : 0,
409 repo.value.defaultBranch,
410 rootDir,
411 primary ? 1 : 0,
412 a.actor.username,
413 at,
414 at,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look415 repo.value.archivedAt ?? null,
API reference examples use projects416 )
417 .run();
418 return ok(toProject((await this.db.prepare("SELECT * FROM projects WHERE id = ?").bind(id).first<Row>())!));
419 }
420
421 async update(a: {
422 actor: User;
423 workspace: string;
424 slug: string;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97425 changes: ProjectChanges;
API reference examples use projects426 }): Promise<Result<Project>> {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97427 const changes = a.changes ?? {};
API reference examples use projects428 const workspace = a.workspace.toLowerCase();
429 const row = await this.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look430 .prepare("SELECT * FROM projects WHERE workspace = ? AND slug = ? AND repo_deleted_at IS NULL")
API reference examples use projects431 .bind(workspace, a.slug.toLowerCase())
432 .first<Row>();
433 if (!row) return fail("not_found", "There is no such project.");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look434 const allowed = this.changeable(row, a.actor, NEEDS.update);
435 if (!allowed.ok) return allowed;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97436 const name = changes.name?.trim() || row.name;
Fast pages, required checks on the branch, self-hosted runners, honest incidents437 // Blank or null goes back to following the repository's description.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97438 const description = ownDescription(row.description, changes.description);
439 const rootDir = changes.rootDir === undefined ? row.root_dir : String(changes.rootDir).trim().replace(/^\/+|\/+$/g, "");
API reference examples use projects440 if (rootDir.split("/").some((part) => part === "..")) return fail("invalid", "The root directory is inside the repository.");
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97441 const known = (value: unknown, allowed: readonly string[], what: string) =>
442 value === undefined || allowed.includes(value as string) ? null : fail("invalid", `${what} is one of ${allowed.join(", ")}.`);
443 const badKind = known(changes.kind, ["auto", "app", "library", "tool", "docs", "other"], "kind");
444 if (badKind) return badKind;
445 const badRuns = known(changes.runs, ["auto", "g1t", "elsewhere"], "runs");
446 if (badRuns) return badRuns;
447 const before = { kind: kindSetting(row.kind_setting), runs: runsSetting(row.runs_setting) };
448 const setting = nextSetting(before, { kind: changes.kind as ProjectKind | "auto" | undefined, runs: changes.runs });
449 // Something that never runs, while Deployments run, would leave apps up
450 // that no page offers: a person turns them off first.
451 if (neverRuns(setting) && !neverRuns(before) && (row.deployments_on ?? (await this.deploymentsOn(row.id)))) {
A project is an app or a library: libraries show their package and how to ship a release, not production452 return fail("conflict", "Deployments are on for this project. Turn them off in its Deployments settings first.");
453 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97454 const url = (value: string | null | undefined, current: string | null | undefined, what: string) =>
455 value === undefined ? { ok: true as const, value: current ?? null } : cleanUrl(value, what);
456 const production = url(changes.productionUrl, row.production_url, "The production address");
457 if (!production.ok) return fail("invalid", production.message);
458 const homepage = url(changes.homepage, row.homepage, "The homepage");
459 if (!homepage.ok) return fail("invalid", homepage.message);
460 const docs = url(changes.docsUrl, row.docs_url, "The docs address");
461 if (!docs.ok) return fail("invalid", docs.message);
462 let linksJson = row.links ?? null;
463 if (changes.links !== undefined) {
464 const links = cleanLinks(changes.links);
465 if (!links.ok) return fail("invalid", links.message);
466 linksJson = links.value.length ? JSON.stringify(links.value) : null;
467 }
API reference examples use projects468 await this.db
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97469 .prepare(
470 `UPDATE projects SET name = ?, description = ?, root_dir = ?, kind_setting = ?, runs_setting = ?, production_url = ?,
471 homepage = ?, docs_url = ?, links = ?, updated_at = ? WHERE id = ?`,
472 )
473 .bind(
474 name.slice(0, 100),
475 description,
476 rootDir,
477 setting.kind,
478 setting.runs,
479 production.value,
480 homepage.value,
481 docs.value,
482 linksJson,
483 now(),
484 row.id,
485 )
API reference examples use projects486 .run();
A project is an app or a library: libraries show their package and how to ship a release, not production487 let saved = (await this.db.prepare("SELECT * FROM projects WHERE id = ?").bind(row.id).first<Row>())!;
488 // Another root has other files: read them again.
489 if (rootDir !== row.root_dir) saved = (await this.detect({ ...saved, detected_commit: null }).catch(() => null)) ?? saved;
490 return ok(toProject(saved));
API reference examples use projects491 }
492
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97493 async publicLinks(a: { repos: { namespace: string; name: string }[] }): Promise<Record<string, string>> {
494 const keys = [...new Set((Array.isArray(a.repos) ? a.repos : []).slice(0, 100).map((r) => `${r.namespace}/${r.name}`.toLowerCase()))];
495 if (keys.length === 0) return {};
496 const rows = await this.db
497 .prepare(
498 `SELECT * FROM projects WHERE is_primary = 1 AND repo_private = 0 AND repo_deleted_at IS NULL
499 AND lower(repo_namespace || '/' || repo_name) IN (${keys.map(() => "?").join(", ")})`,
500 )
501 .bind(...keys)
502 .all<Row>();
503 const out: Record<string, string> = {};
504 for (const row of rows.results) {
505 const project = toProject(row);
506 const link = (project.runs === "elsewhere" ? project.productionUrl : null) ?? project.links.homepage ?? project.links.docs;
507 if (link) out[`${row.repo_namespace}/${row.repo_name}`.toLowerCase()] = link;
508 }
509 return out;
510 }
511
A project is an app or a library: libraries show their package and how to ship a release, not production512 async deploymentsChanged(a: { projectId: string; enabled: boolean }): Promise<void> {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97513 // Turned on, it runs on g1t: one said to run elsewhere moved here.
514 await this.db
515 .prepare(
516 "UPDATE projects SET deployments_on = ?1, runs_setting = CASE WHEN ?1 = 1 AND runs_setting = 'elsewhere' THEN 'g1t' ELSE runs_setting END WHERE id = ?2",
517 )
518 .bind(a.enabled ? 1 : 0, a.projectId)
519 .run();
A project is an app or a library: libraries show their package and how to ship a release, not production520 }
521
Projects: pinned and recent projects, per person per workspace522 // ---- Pinned and recent ---------------------------------------------------
523
524 /** The person's pins in the workspace, in order, as rows: hidden projects left out. */
525 private async pinRows(userId: string, workspace: string): Promise<Row[]> {
526 const rows = await this.db
527 .prepare(
528 `SELECT p.* FROM pins n JOIN projects p ON p.id = n.project_id
529 WHERE n.user_id = ? AND p.workspace = ? AND p.repo_deleted_at IS NULL ORDER BY n.position, n.pinned_at`,
530 )
531 .bind(userId, workspace.toLowerCase())
532 .all<Row>();
533 return rows.results;
534 }
535
536 /** Writes the person's pins in the workspace in this order, by project id. */
537 private async writePins(userId: string, order: string[], dropped: string[] = []): Promise<void> {
538 const at = now();
539 await this.db.batch([
540 ...dropped.map((id) => this.db.prepare("DELETE FROM pins WHERE user_id = ? AND project_id = ?").bind(userId, id)),
541 ...order.map((id, position) =>
542 this.db
543 .prepare(
544 `INSERT INTO pins (user_id, project_id, position, pinned_at) VALUES (?, ?, ?, ?)
545 ON CONFLICT (user_id, project_id) DO UPDATE SET position = excluded.position`,
546 )
547 .bind(userId, id, position, at),
548 ),
549 ]);
550 }
551
552 /** The person behind a call about their own pins, or why there is none. */
553 private person(viewer: Viewer): Result<User> {
554 if (!viewer) return fail("unauthenticated", "Sign in to pin projects.");
555 if (viewer.kind && viewer.kind !== "user") return fail("forbidden", "Pins are a person's own: use a personal access token.");
556 return ok(viewer);
557 }
558
559 /** A project the person can see, for pinning. */
560 private async pinnable(actor: User, workspace: string, slug: string): Promise<Result<Row>> {
561 const row = await this.row(workspace, slug);
562 if (!row || !this.visible(row, actor)) return fail("not_found", "There is no such project.");
563 return ok(row);
564 }
565
566 async shortcuts(a: { workspace: string; viewer: Viewer }): Promise<ProjectShortcuts> {
567 const who = this.person(a.viewer);
568 if (!who.ok) return { pinned: [], recent: [] };
569 const workspace = a.workspace.toLowerCase();
570 const [pinned, visited] = await Promise.all([
571 this.pinRows(who.value.id, workspace),
572 this.db
573 .prepare(
574 `SELECT p.* FROM visits v JOIN projects p ON p.id = v.project_id
575 WHERE v.user_id = ? AND p.workspace = ? AND p.repo_deleted_at IS NULL ORDER BY v.visited_at DESC LIMIT ?`,
576 )
577 .bind(who.value.id, workspace, MAX_PINS + MAX_RECENT)
578 .all<Row>(),
579 ]);
580 const shown = (row: Row) => this.visible(row, who.value);
581 const pins = pinned.filter(shown);
582 return {
583 pinned: pins.map(toProject),
584 recent: recentAfterPins(visited.results.filter(shown), pins.map((row) => row.id)).map(toProject),
585 };
586 }
587
588 async pin(a: { actor: User; workspace: string; slug: string; position?: number | null }): Promise<Result<Project[]>> {
589 const who = this.person(a.actor);
590 if (!who.ok) return who;
591 const found = await this.pinnable(who.value, a.workspace, a.slug);
592 if (!found.ok) return found;
593 const current = await this.pinRows(who.value.id, found.value.workspace);
594 const order = placePin(
595 current.map((row) => row.id),
596 found.value.id,
597 a.position,
598 );
599 if (!order) return fail("conflict", `You can pin ${MAX_PINS} projects in a workspace. Unpin one first.`);
600 await this.writePins(who.value.id, order);
601 return ok((await this.pinRows(who.value.id, found.value.workspace)).filter((row) => this.visible(row, who.value)).map(toProject));
602 }
603
604 async unpin(a: { actor: User; workspace: string; slug: string }): Promise<Result<Project[]>> {
605 const who = this.person(a.actor);
606 if (!who.ok) return who;
607 const row = await this.row(a.workspace, a.slug);
608 if (!row) return fail("not_found", "There is no such project.");
609 const rest = (await this.pinRows(who.value.id, row.workspace)).map((pinned) => pinned.id).filter((id) => id !== row.id);
610 await this.writePins(who.value.id, rest, [row.id]);
611 return ok((await this.pinRows(who.value.id, row.workspace)).filter((pinned) => this.visible(pinned, who.value)).map(toProject));
612 }
613
614 async reorderPins(a: { actor: User; workspace: string; slugs: string[] }): Promise<Result<Project[]>> {
615 const who = this.person(a.actor);
616 if (!who.ok) return who;
617 if (!Array.isArray(a.slugs)) return fail("invalid", "Give the pinned projects' slugs, in order.");
618 const workspace = a.workspace.toLowerCase();
619 const current = await this.pinRows(who.value.id, workspace);
620 const idOf = new Map(current.map((row) => [row.slug, row.id]));
621 const wanted = a.slugs.map((slug) => idOf.get(String(slug).toLowerCase()) ?? String(slug));
622 const order = reorderPins(
623 current.map((row) => row.id),
624 wanted,
625 );
626 if (!order.ok) return fail("invalid", order.message);
627 await this.writePins(who.value.id, order.order);
628 return ok((await this.pinRows(who.value.id, workspace)).filter((row) => this.visible(row, who.value)).map(toProject));
629 }
630
631 async visited(a: { actor: User; projectId: string }): Promise<void> {
632 const who = this.person(a.actor);
633 if (!who.ok || !a.projectId) return;
634 await this.db.batch([
635 this.db
636 .prepare(
637 `INSERT INTO visits (user_id, project_id, visited_at) VALUES (?, ?, ?)
638 ON CONFLICT (user_id, project_id) DO UPDATE SET visited_at = excluded.visited_at`,
639 )
640 .bind(who.value.id, a.projectId, now()),
641 // Only their latest few are kept.
642 this.db
643 .prepare(
644 `DELETE FROM visits WHERE user_id = ?1 AND project_id NOT IN
645 (SELECT project_id FROM visits WHERE user_id = ?1 ORDER BY visited_at DESC LIMIT ?2)`,
646 )
647 .bind(who.value.id, MAX_VISITS),
648 ]);
649 }
650
A project is an app or a library: libraries show their package and how to ship a release, not production651 // ---- App or library --------------------------------------------------
652
653 /**
654 * Reads what decides whether the project is a library: the manifests at
655 * its root at `commit` (the default branch's head when null), a package
656 * its repository publishes, and, the first time, whether Deployments are
657 * on. Files are read again only for a commit they were not read at.
658 * `packages` reads only the packages. Returns the row as it is now.
659 */
660 private async detect(row: Row, commit: string | null = null, only?: "packages"): Promise<Row> {
661 const actor = await this.workspaceActor(row.workspace);
662 if (!actor) return row;
663 const repo = { namespace: row.repo_namespace, name: row.repo_name };
664 const sets: string[] = [];
665 const values: unknown[] = [];
666 const set = (column: string, value: unknown) => {
667 sets.push(`${column} = ?`);
668 values.push(value);
669 };
670 const filesRead = only === "packages" || (commit != null && commit === row.detected_commit);
671 const [files, linked, deploying] = await Promise.all([
672 filesRead ? null : this.readRoot(repo, actor, commit, row.root_dir),
673 this.linkedPackage(row, actor),
674 row.deployments_on == null && only !== "packages" ? this.deploymentsOn(row.id) : null,
675 ]);
676 if (files) {
677 const found = files.commit ? detectKind(files.root) : null;
678 set("detected_kind", found?.kind ?? null);
679 set("detected_detail", found?.detail ?? null);
680 set("detected_ecosystem", found?.ecosystem ?? null);
681 set("detected_commit", files.commit);
682 }
683 if (linked !== undefined) set("linked_package", linked);
684 if (deploying != null) set("deployments_on", deploying ? 1 : 0);
685 if (sets.length === 0) return row;
686 await this.db
687 .prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`)
688 .bind(...values, row.id)
689 .run();
690 return (await this.db.prepare("SELECT * FROM projects WHERE id = ?").bind(row.id).first<Row>()) ?? row;
691 }
692
693 /** The project's root at a commit: its names, and the few files detection reads. Commit '' when it has none. */
694 private async readRoot(
695 repo: { namespace: string; name: string },
696 actor: User,
697 commit: string | null,
698 rootDir: string,
699 ): Promise<{ commit: string; root: RootFiles } | null> {
700 const client = reposClient(this.env.REPOS);
701 const empty = { commit: "", root: { entries: [], text: {} } };
702 const tree = await client.tree(repo, actor, commit, rootDir);
703 if (!tree.ok) return null;
704 const head = commit ?? tree.value.head?.hash ?? "";
705 if (!head) return empty;
706 const entries = tree.value.entries.map((entry) => (entry.kind === "tree" ? `${entry.name}/` : entry.name));
707 const at = (name: string) => (rootDir ? `${rootDir}/${name}` : name);
708 const names = [...MANIFESTS.filter((name) => entries.includes(name)), ...(entries.includes("go.mod") ? goFilesToRead(entries) : [])];
709 const below = (dir: string) => client.tree(repo, actor, head, at(dir)).catch(() => null);
710 const [texts, src, pub] = await Promise.all([
711 Promise.all(
712 names.map(async (name) => {
713 const blob = await client.blob(repo, actor, head, at(name)).catch(() => null);
714 return [name, blob?.ok ? (blob.value.text ?? "") : ""] as const;
715 }),
716 ),
717 entries.includes("Cargo.toml") && entries.includes("src/") ? below("src") : null,
718 entries.includes("composer.json") && entries.includes("public/") ? below("public") : null,
719 ]);
720 const list = (view: Awaited<ReturnType<typeof below>>) => (view?.ok ? view.value.entries.map((entry) => entry.name) : undefined);
721 return { commit: head, root: { entries, text: Object.fromEntries(texts), src: list(src), public: list(pub) } };
722 }
723
724 /** A package other than a container image that the repository publishes, named; undefined when packages could not say. */
725 private async linkedPackage(row: Row, actor: User): Promise<string | null | undefined> {
726 const listed = await packagesClient(this.env.PACKAGES)
727 .list(row.workspace, actor, { repoId: row.repo_id })
728 .catch(() => null);
729 if (!listed?.ok) return undefined;
730 // An image is how an app ships too; it says nothing about being a library.
731 const pkg = listed.value.find((p) => p.ecosystem !== "container");
732 if (!pkg) return null;
733 return `${ECOSYSTEM_NAME[pkg.ecosystem]} ${pkg.ecosystem === "npm" ? `@${pkg.workspace}/${pkg.name}` : pkg.name}`;
734 }
735
736 /** Whether Deployments are on for the project, asked of deployments once; after that it tells this service. */
737 private async deploymentsOn(projectId: string): Promise<boolean | null> {
738 const answer = await this.env.DEPLOYMENTS.fetch("https://deployments/rpc/is_enabled", {
739 method: "POST",
740 headers: { "content-type": "application/json" },
741 body: JSON.stringify({ projectId }),
742 }).catch(() => null);
743 if (!answer?.ok) return null;
744 const value = (await answer.json().catch(() => null)) as unknown;
745 return typeof value === "boolean" ? value : null;
746 }
Project dependencies: addresses, preview stacks, Affects, and agents who know747
748 private async row(workspace: string, slug: string): Promise<Row | null> {
749 return this.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look750 .prepare("SELECT * FROM projects WHERE workspace = ? AND slug = ? AND repo_deleted_at IS NULL")
Project dependencies: addresses, preview stacks, Affects, and agents who know751 .bind(workspace.toLowerCase(), slug.toLowerCase())
752 .first<Row>();
753 }
754
Projects: when each was last pushed, and how active it is lately755 /** Something happened to a repository's code or work: its projects are that much more active. */
756 private async active(repoId: string, at: string, pushed: boolean): Promise<void> {
757 const rows = await this.db
758 .prepare("SELECT id, activity, active_at FROM projects WHERE repo_id = ?")
759 .bind(repoId)
760 .all<{ id: string; activity: number | null; active_at: string | null }>();
761 if (rows.results.length === 0) return;
762 await this.db.batch(
763 rows.results.map((row) => {
764 const next = raised(row.activity ?? 0, row.active_at, at);
765 return pushed
766 ? this.db
767 .prepare("UPDATE projects SET activity = ?, active_at = ?, pushed_at = MAX(COALESCE(pushed_at, ''), ?) WHERE id = ?")
768 .bind(next.score, next.at, at, row.id)
769 : this.db.prepare("UPDATE projects SET activity = ?, active_at = ? WHERE id = ?").bind(next.score, next.at, row.id);
770 }),
771 );
772 }
773
API reference examples use projects774 async onEvent(event: G1tEvent): Promise<void> {
Projects: when each was last pushed, and how active it is lately775 if (ACTIVE.has(event.type)) {
776 const repoId = event.repoId ?? ("repoId" in event.data ? (event.data as { repoId?: string }).repoId : undefined);
777 if (repoId) await this.active(repoId, event.time, event.type === "git.push");
778 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains779 if (event.type === "workspace.renamed") {
780 const current = await currentWorkspaceSlug(this.env.IDENTITY, event.data);
781 const statements = renameStatements(staleSlugs(event.data, current), current);
782 if (statements.length) await this.db.batch(statements.map(({ sql, params }) => this.db.prepare(sql).bind(...params)));
783 return;
784 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 const move = repoMove(event);
786 if (move) {
787 const current = await currentMovedPath(this.env.REPOS, move);
788 const stale = staleMovedPaths(move, current);
789 if (stale.length === 0) return;
790 const statements = moveStatements(stale, current, move.repoId);
791 await this.db.batch(statements.map(({ sql, params }) => this.db.prepare(sql).bind(...params)));
792 // A project whose slug the destination already had moves under a free one.
793 const query = strandedQuery(stale, current, move.repoId);
794 if (!query) return;
795 const workspace = current.split("/")[0]!;
796 const stranded = await this.db.prepare(query.sql).bind(...query.params).all<Row>();
797 for (const row of stranded.results) {
798 const slug = await this.freeSlug(workspace, row.slug);
799 console.log("project", row.id, "moved to", workspace, "as", slug, "since", row.slug, "was taken");
800 await this.db
801 .prepare("UPDATE projects SET workspace = ?, slug = ?, repo_namespace = ?, updated_at = ? WHERE id = ?")
802 .bind(workspace, slug, workspace, now(), row.id)
803 .run();
804 }
805 return;
806 }
807 if (event.type === "repo.deleted") {
808 // Hidden, not gone: a restore brings its projects back as they were.
809 await this.db
810 .prepare("UPDATE projects SET repo_deleted_at = COALESCE(repo_deleted_at, ?) WHERE repo_id = ?")
811 .bind(now(), event.data.repoId)
812 .run();
813 return;
814 }
815 if (event.type === "repo.restored") {
816 await this.db
817 .prepare("UPDATE projects SET repo_deleted_at = NULL, repo_private = ? WHERE repo_id = ?")
818 .bind(event.data.isPrivate ? 1 : 0, event.data.repoId)
819 .run();
820 return;
821 }
822 if (event.type === "repo.purged") {
823 const ids = "SELECT id FROM projects WHERE repo_id = ?1";
824 await this.db.batch([
Projects: pinned and recent projects, per person per workspace825 // Pins and visits of what is gone go with it.
826 this.db.prepare(`DELETE FROM pins WHERE project_id IN (${ids})`).bind(event.data.repoId),
827 this.db.prepare(`DELETE FROM visits WHERE project_id IN (${ids})`).bind(event.data.repoId),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look828 this.db.prepare("DELETE FROM projects WHERE repo_id = ?").bind(event.data.repoId),
829 ]);
830 return;
831 }
832 if (event.type === "repo.updated" || event.type === "repo.visibility_changed") {
833 // Who may see its projects follows who may see the repository.
834 await this.db
835 .prepare("UPDATE projects SET repo_private = ? WHERE repo_id = ?")
836 .bind(event.data.isPrivate ? 1 : 0, event.data.repoId)
837 .run();
Fast pages, required checks on the branch, self-hosted runners, honest incidents838 if (event.type === "repo.updated") {
839 // Projects without a description of their own show the repository's.
840 // Asked of repos, so changes delivered out of order end the same.
841 const repo = await this.repoById(event.data.repoId);
842 if (repo) {
843 await this.db
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97844 .prepare("UPDATE projects SET repo_description = ?, repo_website = ? WHERE repo_id = ?")
845 .bind(repo.description ?? null, repo.website ?? null, event.data.repoId)
Fast pages, required checks on the branch, self-hosted runners, honest incidents846 .run();
847 }
848 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look849 return;
850 }
851 if (event.type === "repo.archived" || event.type === "repo.unarchived") {
852 // Asked of repos, so changes delivered out of order end the same.
853 const repo = await this.repoById(event.data.repoId);
854 const archivedAt = repo ? (repo.archivedAt ?? null) : event.data.archived ? now() : null;
855 await this.db.prepare("UPDATE projects SET repo_archived_at = ? WHERE repo_id = ?").bind(archivedAt, event.data.repoId).run();
856 return;
857 }
858 if (event.type === "repo.default_branch_changed") {
859 // Asked of repos, so changes delivered out of order end the same.
860 const repo = await this.repoById(event.data.repoId);
861 await this.db
862 .prepare("UPDATE projects SET default_branch = ? WHERE repo_id = ?")
863 .bind(repo?.defaultBranch ?? event.data.to, event.data.repoId)
864 .run();
A project is an app or a library: libraries show their package and how to ship a release, not production865 // Another branch has other files: they are read again from its head.
866 const rows = await this.db.prepare("SELECT * FROM projects WHERE repo_id = ?").bind(event.data.repoId).all<Row>();
867 for (const row of rows.results) await this.detect({ ...row, detected_commit: null });
868 return;
869 }
870 if (
871 (event.type === "package.published" || event.type === "package.deleted" || event.type === "package.version_deleted") &&
872 event.data.repoId
873 ) {
874 const rows = await this.db.prepare("SELECT * FROM projects WHERE repo_id = ?").bind(event.data.repoId).all<Row>();
875 for (const row of rows.results) await this.detect(row, null, "packages");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look876 return;
877 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member878 if (event.type === "workspace.deleting") {
879 // Hidden, not gone: every project it shows, including any building
880 // from a repository it transferred away, until staff restore it or it
881 // is purged. Those already hidden stay as they were.
882 if (isProtectedWorkspace(event.data.slug)) return;
883 const at = now();
884 await this.db
885 .prepare(
886 "UPDATE projects SET repo_deleted_at = ?1, workspace_deleted_at = ?1 WHERE workspace = ?2 AND repo_deleted_at IS NULL",
887 )
888 .bind(at, event.data.slug.toLowerCase())
889 .run();
890 return;
891 }
892 if (event.type === "workspace.restored") {
893 await this.db
894 .prepare(
895 "UPDATE projects SET repo_deleted_at = NULL, workspace_deleted_at = NULL WHERE workspace = ? AND workspace_deleted_at IS NOT NULL",
896 )
897 .bind(event.data.slug.toLowerCase())
898 .run();
899 return;
900 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look901 if (event.type === "workspace.deleted") {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member902 // Its own repositories' projects go with them (`repo.purged`); any
903 // building from a repository it transferred away goes now. It is not
904 // backfilled again.
905 const slug = event.data.slug.toLowerCase();
906 const ids = "SELECT id FROM projects WHERE workspace = ?1";
907 await this.db.batch([
Projects: pinned and recent projects, per person per workspace908 this.db.prepare(`DELETE FROM pins WHERE project_id IN (${ids})`).bind(slug),
909 this.db.prepare(`DELETE FROM visits WHERE project_id IN (${ids})`).bind(slug),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member910 this.db.prepare("DELETE FROM projects WHERE workspace = ?").bind(slug),
911 this.db.prepare("DELETE FROM backfilled WHERE workspace = ?").bind(slug),
912 ]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look913 return;
914 }
Project dependencies: addresses, preview stacks, Affects, and agents who know915 if (event.type === "git.push" && event.data.defaultBranch) {
916 const rows = await this.db.prepare("SELECT * FROM projects WHERE repo_id = ?").bind(event.data.repoId).all<Row>();
A project is an app or a library: libraries show their package and how to ship a release, not production917 for (const row of rows.results) {
918 await this.detect(row, event.data.after);
919 }
Project dependencies: addresses, preview stacks, Affects, and agents who know920 return;
921 }
API reference examples use projects922 if (event.type !== "repo.created") return;
923 const actor = await this.workspaceActor(event.data.namespace);
924 if (!actor) return;
925 const repo = await reposClient(this.env.REPOS).get({ namespace: event.data.namespace, name: event.data.name }, actor);
926 if (repo.ok) await this.ensureFor(repo.value, event.actor ?? "g1t");
927 }
928}
929
Fast pages, required checks on the branch, self-hosted runners, honest incidents930/** One RPC method's answer. */
931async function answer(service: Projects, method: string, args: any): Promise<Response> {
932 switch (method) {
933 case "list":
934 return Response.json(await service.list(args));
935 case "get":
936 return Response.json(await service.get(args));
937 case "by_repo":
938 return Response.json(await service.byRepo(args));
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member939 case "count":
940 return Response.json(await service.count(args));
Fast pages, required checks on the branch, self-hosted runners, honest incidents941 case "create":
942 return Response.json(await service.create(args));
943 case "update":
944 return Response.json(await service.update(args));
A project is an app or a library: libraries show their package and how to ship a release, not production945 case "deployments_changed":
946 await service.deploymentsChanged(args);
947 return Response.json(null);
Projects: pinned and recent projects, per person per workspace948 case "shortcuts":
949 return Response.json(await service.shortcuts(args));
950 case "pin":
951 return Response.json(await service.pin(args));
952 case "unpin":
953 return Response.json(await service.unpin(args));
954 case "reorder_pins":
955 return Response.json(await service.reorderPins(args));
956 case "visited":
957 await service.visited(args);
958 return Response.json(null);
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97959 case "public_links":
960 return Response.json(await service.publicLinks(args));
Fast pages, required checks on the branch, self-hosted runners, honest incidents961 default:
962 return new Response("Unknown method\n", { status: 404 });
963 }
964}
965
API reference examples use projects966export default {
A project is an app or a library: libraries show their package and how to ship a release, not production967 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
API reference examples use projects968 const match = new URL(request.url).pathname.match(/^\/rpc\/([a-z_]+)$/);
969 if (request.method !== "POST" || !match) return new Response("Not found\n", { status: 404 });
Fast pages, required checks on the branch, self-hosted runners, honest incidents970 // A replica near the caller when it asks for one (@g1t/contracts d1.ts).
971 const opened = openD1(env.DB, request);
A project is an app or a library: libraries show their package and how to ship a release, not production972 const service = new Projects(Object.create(env, { DB: { value: opened.db } }) as Env, (work) => ctx.waitUntil(work));
API reference examples use projects973 const args = (await request.json().catch(() => ({}))) as any;
Fast pages, required checks on the branch, self-hosted runners, honest incidents974 return opened.finish(await answer(service, match[1], args));
API reference examples use projects975 },
976
977 async queue(batch: MessageBatch<G1tEvent>, env: Env): Promise<void> {
978 const service = new Projects(env);
979 for (const message of batch.messages) {
980 try {
981 await service.onEvent(message.body);
982 message.ack();
983 } catch (error) {
984 console.error("projects could not handle", message.body.type, error);
985 message.retry();
986 }
987 }
988 },
989} satisfies ExportedHandler<Env, G1tEvent>;

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