Public: anyone can read this; the crew can change it.
A trusted source on this same forge folds in like one on another forge #9552
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9450-a-trusted-source-on-this-same-forge-fold → main 1 commit fork
3 files
changed +156
−4
src/app.ts +2
| @@ -4,6 +4,7 @@ | ||
| 4 | 4 | * side-effect-contained, so tests boot real forges by the dozen. |
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | import { ownHost, setOwnHost } from "./core/trusted_sources.ts"; | |
| 7 | 8 | import { commitIdentity } from "./core/commit_identity.ts"; |
| 8 | 9 | import { getUserById } from "./core/users.ts"; |
| 9 | 10 | import type { MailConfig, Mailer } from "./kernel/mail.ts"; |
| @@ -239,6 +240,7 @@ export function createApp(config: ForgeConfig): App { | ||
| 239 | 240 | const dbPath = join(dataDir, "forge.db"); |
| 240 | 241 | const fresh = !existsSync(dbPath); |
| 241 | 242 | const db = openDb(dbPath, migrationFiles); |
| 243 | setOwnHost(db, ownHost(config)); | |
| 242 | 244 | if (config.seedSignup === true) seedSignup(db, fresh); |
| 243 | 245 | // Hand-installed new-account guards become the `guard_new_accounts` |
| 244 | 246 | // setting (default off): drop the legacy triggers once, here, on boot. |
src/core/trusted_sources.ts +34 −1
| @@ -275,8 +275,41 @@ export function pullIdsFrom(db: Database, host: string, account: string): string | ||
| 275 | 275 | * person's review and nothing else. |
| 276 | 276 | */ |
| 277 | 277 | export function foldsIn(db: Database, pullId: string, now: Date = new Date()): boolean { |
| 278 | if (foldInStopped(db) !== undefined) return false; | |
| 278 | 279 | const from = pullSourceOf(db, pullId); |
| 279 | return from !== undefined && foldInStopped(db) === undefined && isTrusted(db, from.host, from.account, now); | |
| 280 | if (from !== undefined) return isTrusted(db, from.host, from.account, now); | |
| 281 | return localSourceTrusted(db, pullId, now); | |
| 282 | } | |
| 283 | ||
| 284 | // --- trust for work opened on this same forge -------------------------------- | |
| 285 | ||
| 286 | const selfHosts = new WeakMap<Database, string>(); | |
| 287 | ||
| 288 | /** Tell the trust layer which name this forge goes by in trust lists (called once, where the app is made). */ | |
| 289 | export function setOwnHost(db: Database, host: string): void { | |
| 290 | selfHosts.set(db, host.toLowerCase()); | |
| 291 | } | |
| 292 | ||
| 293 | /** | |
| 294 | * A pull opened here, from the opener's own fork on this forge (not a | |
| 295 | * mirror's), by an account trusted for this forge's own host. Trust is read | |
| 296 | * now, so a lapse or a removal makes the next question answer no. | |
| 297 | */ | |
| 298 | function localSourceTrusted(db: Database, pullId: string, now: Date): boolean { | |
| 299 | const self = selfHosts.get(db); | |
| 300 | if (self === undefined) return false; | |
| 301 | const row = db | |
| 302 | .query<{ account: string }, [string]>( | |
| 303 | `SELECT u.username AS account FROM issue i | |
| 304 | JOIN pull p ON p.issue_id = i.id | |
| 305 | JOIN user u ON u.id = i.author_id | |
| 306 | JOIN repo h ON h.id = p.head_repo_id | |
| 307 | WHERE i.id = ?1 AND i.kind = 'pull' | |
| 308 | AND p.head_repo_id <> i.repo_id AND p.origin_head_repo IS NULL | |
| 309 | AND h.owner_id = i.author_id AND h.fork_parent_id = i.repo_id`, | |
| 310 | ) | |
| 311 | .get(pullId); | |
| 312 | return row !== null && isTrusted(db, self, row.account, now); | |
| 280 | 313 | } |
| 281 | 314 | |
| 282 | 315 | /** |
test/trusted-fold.test.ts +120 −3
| @@ -9,16 +9,17 @@ import { mkdtemp, rm } from "node:fs/promises"; | ||
| 9 | 9 | import { tmpdir } from "node:os"; |
| 10 | 10 | import { join } from "node:path"; |
| 11 | 11 | import { openPull } from "../src/actions/pulls.ts"; |
| 12 | import { commitToBranch, createRepo } from "../src/actions/repos.ts"; | |
| 12 | import { commitToBranch, createRepo, forkRepo } from "../src/actions/repos.ts"; | |
| 13 | 13 | import { addTrust, setFoldInStopped } from "../src/actions/trusted_sources.ts"; |
| 14 | 14 | import { createApp, type App } from "../src/app.ts"; |
| 15 | 15 | import { provideChecksForTest, type ChecksHandle } from "../src/checks/service.ts"; |
| 16 | 16 | import { createSession } from "../src/core/sessions.ts"; |
| 17 | import { pullMayRun } from "../src/core/pull_trust.ts"; | |
| 17 | 18 | import { getPull, type Pull } from "../src/core/pulls.ts"; |
| 18 | 19 | import { getRepoById, type Repo } from "../src/core/repos.ts"; |
| 19 | 20 | import { claimRun, createRun, finishRun } from "../src/core/runs.ts"; |
| 20 | import { ownHost, recordPullSource, takeable, viaOf } from "../src/core/trusted_sources.ts"; | |
| 21 | import { createUser, type User } from "../src/core/users.ts"; | |
| 21 | import { foldsIn, ownHost, recordPullSource, takeable, viaOf } from "../src/core/trusted_sources.ts"; | |
| 22 | import { createUser, getUserById, type User } from "../src/core/users.ts"; | |
| 22 | 23 | import { landingStrategy, sweepGreen } from "../src/crew/autonomy.ts"; |
| 23 | 24 | import { runGitCapture } from "../src/git/env.ts"; |
| 24 | 25 | import { repoDir } from "../src/git/repos.ts"; |
| @@ -289,3 +290,119 @@ describe("the landing reads the clock it is given", () => { | ||
| 289 | 290 | expect(await landingStrategy(f.app, f.repo, pull, base, new Date("2099-12-03T00:00:00Z"))).toBe("squash"); |
| 290 | 291 | }); |
| 291 | 292 | }); |
| 293 | ||
| 294 | describe("a trusted account working on this same forge (#9450)", () => { | |
| 295 | let f: Forge; | |
| 296 | let open: Repo; | |
| 297 | let bot: User; | |
| 298 | let carol: User; | |
| 299 | let n = 0; | |
| 300 | const HOME = "forge.test"; | |
| 301 | ||
| 302 | beforeAll(async () => { | |
| 303 | f = await forge(HOME); | |
| 304 | const made = await createRepo(f.app, f.alice, { name: "open", visibility: "public" }); | |
| 305 | if (!made.ok) throw new Error(String(made.error)); | |
| 306 | const seeded = await commitToBranch(f.app, f.alice, made.value, { | |
| 307 | message: "Start", | |
| 308 | files: [ | |
| 309 | { path: "README.md", content: "# open\n" }, | |
| 310 | { path: "forge.json", content: JSON.stringify({ check: { run: "true", fast: "true" } }) }, | |
| 311 | ], | |
| 312 | }); | |
| 313 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 314 | f.app.db.query("UPDATE repo SET autonomy = 'autonomous', autonomy_by = ?2 WHERE id = ?1").run(made.value.id, f.alice.id); | |
| 315 | open = getRepoById(f.app.db, made.value.id)!; | |
| 316 | const people: User[] = []; | |
| 317 | for (const username of ["grokbot", "carol"]) { | |
| 318 | const u = await createUser(f.app.db, { | |
| 319 | origin: "seed", | |
| 320 | trusted: true, | |
| 321 | username, | |
| 322 | email: `${username}@forge.local`, | |
| 323 | password: "a-good-passphrase", | |
| 324 | }); | |
| 325 | if (!u.ok) throw new Error(u.error); | |
| 326 | f.app.db.query("UPDATE user SET created_at = ?2 WHERE id = ?1").run(u.value.id, "2020-01-01T00:00:00.000Z"); | |
| 327 | people.push(getUserById(f.app.db, u.value.id)!); | |
| 328 | } | |
| 329 | [bot, carol] = people as [User, User]; | |
| 330 | const t = addTrust(f.app, f.alice, { host: HOME, account: "grokbot", until: "2099-12-02" }); | |
| 331 | if (!t.ok) throw new Error(t.error); | |
| 332 | }); | |
| 333 | ||
| 334 | /** `who`'s pull from their own fork of `open`, with a green check. */ | |
| 335 | async function forkPull(who: User, file = `src/x${++n}.ts`): Promise<Pull> { | |
| 336 | const forks = f.app.db | |
| 337 | .query<{ id: string }, [string, string]>("SELECT id FROM repo WHERE owner_id = ?1 AND fork_parent_id = ?2") | |
| 338 | .get(who.id, open.id); | |
| 339 | let fork = forks === null ? undefined : getRepoById(f.app.db, forks.id); | |
| 340 | if (fork === undefined) { | |
| 341 | const forked = await forkRepo(f.app, who, open, `${who.username}-open`); | |
| 342 | if (!forked.ok) throw new Error(String(forked.error)); | |
| 343 | fork = forked.value; | |
| 344 | } | |
| 345 | const branch = `w${++n}`; | |
| 346 | const pushed = await commitToBranch(f.app, who, fork, { | |
| 347 | branch, | |
| 348 | message: `Add ${file}`, | |
| 349 | files: [{ path: file, content: "export const n = 1;\n" }], | |
| 350 | }); | |
| 351 | if (!pushed.ok) throw new Error(JSON.stringify(pushed.error)); | |
| 352 | const opened = await openPull(f.app, who, open, { title: branch, baseBranch: "main", headBranch: branch, headRepo: fork }); | |
| 353 | if (!opened.ok) throw new Error(String(opened.error)); | |
| 354 | const pull = getPull(f.app.db, open.id, opened.value.number)!; | |
| 355 | const run = createRun(f.app.db, { | |
| 356 | repoId: open.id, | |
| 357 | kind: "check", | |
| 358 | issueId: pull.id, | |
| 359 | env: `pull-${pull.number}`, | |
| 360 | commitOid: pull.headOid, | |
| 361 | reason: "pull", | |
| 362 | }); | |
| 363 | claimRun(f.app.db, run.id); | |
| 364 | finishRun(f.app.db, run.id, { state: "ok", summary: "ok" }); | |
| 365 | return pull; | |
| 366 | } | |
| 367 | ||
| 368 | const landed = (pull: Pull): boolean => getPull(f.app.db, open.id, pull.number)!.mergedAt !== null; | |
| 369 | ||
| 370 | test("its pull from its own fork runs its checks and folds in by true merge", async () => { | |
| 371 | const pull = await forkPull(bot); | |
| 372 | expect(pullMayRun(f.app.db, open, pull, pull.headOid)).toBe(true); | |
| 373 | expect(await sweepGreen(f.app, open)).toBe(1); | |
| 374 | expect(landed(pull)).toBe(true); | |
| 375 | const dir = repoDir(f.app.reposRoot, open.ownerName, open.name); | |
| 376 | const parents = (await runGitCapture(["log", "-1", "--format=%P", "refs/heads/main"], { cwd: dir })).stdout.trim().split(" "); | |
| 377 | expect(parents.length).toBe(2); | |
| 378 | }); | |
| 379 | ||
| 380 | test("the walls still hold it for a person", async () => { | |
| 381 | const pull = await forkPull(bot, "src/crew/fold_walls.ts"); | |
| 382 | expect(await sweepGreen(f.app, open)).toBe(0); | |
| 383 | expect(landed(pull)).toBe(false); | |
| 384 | }); | |
| 385 | ||
| 386 | test("a guest's pull still waits for approval", async () => { | |
| 387 | const pull = await forkPull(carol); | |
| 388 | expect(pullMayRun(f.app.db, open, pull, pull.headOid)).toBe(false); | |
| 389 | expect(await sweepGreen(f.app, open)).toBe(0); | |
| 390 | expect(landed(pull)).toBe(false); | |
| 391 | }); | |
| 392 | ||
| 393 | test("stopping fold-in makes the next pull wait again", async () => { | |
| 394 | const pull = await forkPull(bot); | |
| 395 | setFoldInStopped(f.app, f.alice, true); | |
| 396 | expect(pullMayRun(f.app.db, open, pull, pull.headOid)).toBe(false); | |
| 397 | expect(await sweepGreen(f.app, open)).toBe(0); | |
| 398 | setFoldInStopped(f.app, f.alice, false); | |
| 399 | expect(await sweepGreen(f.app, open)).toBe(1); | |
| 400 | }); | |
| 401 | ||
| 402 | test("a lapsed trust makes the next pull wait again", async () => { | |
| 403 | const pull = await forkPull(bot); | |
| 404 | expect(await sweepGreen(f.app, open, new Date("2100-01-01T00:00:00Z"))).toBe(0); | |
| 405 | expect(landed(pull)).toBe(false); | |
| 406 | expect(foldsIn(f.app.db, pull.id, new Date("2100-01-01T00:00:00Z"))).toBe(false); | |
| 407 | }); | |
| 408 | }); |
new above
offline