diff --git a/.github/client-overlay-ownership.json b/.github/client-overlay-ownership.json new file mode 100644 index 00000000000..8e91331afa4 --- /dev/null +++ b/.github/client-overlay-ownership.json @@ -0,0 +1,40 @@ +{ + "overlays": [ + { + "id": "desktop-links", + "branch": "t3-discord/f7d37879-desktop-deeplinks", + "pullRequest": 10, + "paths": [ + "apps/desktop/src/app/DesktopApp.ts", + "apps/desktop/src/app/DesktopClerk.test.ts", + "apps/desktop/src/app/DesktopClerk.ts", + "apps/desktop/src/app/DesktopDeepLinks.test.ts", + "apps/desktop/src/app/DesktopDeepLinks.ts", + "apps/desktop/src/backend/DesktopBackendPool.test.ts", + "apps/desktop/src/electron/ElectronProtocol.ts", + "apps/desktop/src/main.ts", + "apps/desktop/src/window/DesktopApplicationMenu.test.ts", + "apps/desktop/src/window/DesktopWindow.test.ts", + "apps/desktop/src/window/DesktopWindow.ts", + "scripts/build-desktop-artifact.ts" + ] + }, + { + "id": "discord", + "branch": "fork/discord", + "pullRequest": null, + "paths": [ + "apps/discord-bot/**", + "docs/integrations/discord-bot.md", + "docs/architecture/discord-browser-automation.md", + "docs/examples/project-aliases.yaml" + ] + }, + { + "id": "vscode", + "branch": "fork/vscode", + "pullRequest": null, + "paths": ["apps/vscode/**", ".vscode/launch.json"] + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 4ffa21efb0f..4b5928fb131 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,11 @@ branches. a registered overlay directly. Update its branch, or use `pnpm fork:stack overlay-start ` and target the child PR at the overlay branch. Draft state blocks merging while normal green CI remains meaningful. +- Before targeting `fork/changes`, inspect `.github/client-overlay-ownership.json` or run + `pnpm fork:overlay-owner [changed-path...]`. Changes owned by an extracted client + must update that draft overlay (or a child PR targeting it), not duplicate its implementation in + `fork/changes`. Read [docs/client-overlays.md](./docs/client-overlays.md) for mixed shared/client + changes and extraction cutovers. - Start new work with `pnpm fork:stack start ` and open the PR against `fork/changes`. Ordinary feature/import PRs are not added to `.github/pr-stack.json`; they enter the runnable fork only after being reviewed and merged into `fork/changes`. diff --git a/docs/client-overlays.md b/docs/client-overlays.md new file mode 100644 index 00000000000..78cc2b17f18 --- /dev/null +++ b/docs/client-overlays.md @@ -0,0 +1,46 @@ +# Client integration overlays + +Discord and VS Code are long-lived product integrations rather than anonymous files in +`fork/changes`. Their complete client implementations live in parallel draft PRs based on +`fork/changes` and are composed into `fork/integration` like the desktop-link overlay. + +Path ownership is recorded in +[`client-overlay-ownership.json`](../.github/client-overlay-ownership.json). Before choosing a base +branch, run: + +```sh +pnpm fork:overlay-owner [changed-path...] +``` + +- `fork/changes` means no extracted client owns the path. +- A PR number means start a child with + `pnpm fork:stack overlay-start ` and merge that child into the overlay. +- `extraction pending` is used only during the reviewed cutover. Do not add new implementation to + `fork/changes`; finish or update the extraction first. + +Shared contracts and runtime behavior stay in `fork/changes` unless they exist solely for one +integration. A feature spanning shared code and an extracted client is split into two PRs: the +shared prerequisite targets `fork/changes`, and the client child targets its overlay. The client PR +may temporarily depend on the shared PR and is rebased once that prerequisite lands. + +The overlay PRs remain draft so they cannot be merged accidentally while still receiving normal CI. +Register their real PR numbers under `integrationOverlays` in `pr-stack.json` and replace the +temporary `null` ownership entries as part of the final cutover. + +## Build and deployment ownership + +Each overlay owns the code and repository-local build metadata required to produce its client: + +- Discord owns `apps/discord-bot/**` and its operator-facing integration documentation. +- VS Code owns `apps/vscode/**` and the repository launch configuration in `.vscode/launch.json`. +- Workspace importer and lockfile changes travel with the overlay that introduces the app. + +Cross-client classification remains shared in `scripts/classify-deployment-diff.sh`; it cannot live +in either client overlay because it decides between server, Discord, VS Code, mobile, and desktop. + +Fleet installation, credentials, systemd units, host names, and artifact distribution remain in the +private `aaaomega/ops` repository. In particular, `scripts/deploy-fork-integration.sh`, +`scripts/build-and-deploy-vscode.sh`, `scripts/publish-fork-workstation-artifacts.sh`, and the guest +Discord service configuration consume the tested, composed `fork/integration` tree. They are +deployment infrastructure, not public client implementation, and therefore are not duplicated into +the product overlays. diff --git a/docs/fork-stack.md b/docs/fork-stack.md index 12fe77968eb..c9919dbe8de 100644 --- a/docs/fork-stack.md +++ b/docs/fork-stack.md @@ -45,6 +45,11 @@ Landing an overlay is deliberate: remove its manifest entry in the same reviewed the implementation in `fork/changes`, then verify that the resulting `fork/integration` tree is unchanged. +Some overlays also own complete client integrations. Their path ownership and change-routing rules +live in [client-overlays.md](./client-overlays.md). Check that ownership before starting ordinary +work so Discord, VS Code, and desktop-link changes do not accidentally leak back into +`fork/changes`. + ## Updating from upstream Do not use GitHub's **Sync fork** button, create a PR into this repository's `main`, or push `main` diff --git a/package.json b/package.json index 65edfd96872..a3ac6f412ec 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64", "release:smoke": "node scripts/release-smoke.ts", "fork:stack": "node scripts/fork-stack.ts", + "fork:overlay-owner": "node scripts/client-overlay-owner.ts", "fork:stack:sync": "node scripts/rebase-pr-stack.ts sync --dry-run", "clean": "rm -rf node_modules apps/*/node_modules packages/*/node_modules apps/*/dist apps/*/dist-electron packages/*/dist .vite-plus apps/*/.vite-plus packages/*/.vite-plus", "sync:repos": "node scripts/sync-reference-repos.ts" diff --git a/scripts/client-overlay-owner.test.ts b/scripts/client-overlay-owner.test.ts new file mode 100644 index 00000000000..8b59ff85678 --- /dev/null +++ b/scripts/client-overlay-owner.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + ownersForPaths, + pathMatchesOwnershipPattern, + type ClientOverlayOwnership, +} from "./client-overlay-owner.ts"; + +const overlays: ReadonlyArray = [ + { + id: "discord", + branch: "fork/discord", + pullRequest: null, + paths: ["apps/discord-bot/**", "docs/integrations/discord-bot.md"], + }, + { + id: "vscode", + branch: "fork/vscode", + pullRequest: 99, + paths: ["apps/vscode/**"], + }, +]; + +describe("client overlay ownership", () => { + it("matches exact files and recursive directory patterns", () => { + expect(pathMatchesOwnershipPattern("apps/discord-bot/src/main.ts", "apps/discord-bot/**")).toBe( + true, + ); + expect( + pathMatchesOwnershipPattern( + "docs/integrations/discord-bot.md", + "docs/integrations/discord-bot.md", + ), + ).toBe(true); + expect(pathMatchesOwnershipPattern("apps/discord/src/main.ts", "apps/discord-bot/**")).toBe( + false, + ); + }); + + it("finds every overlay touched by a mixed change", () => { + expect( + ownersForPaths(overlays, [ + "packages/contracts/src/orchestration.ts", + "apps/discord-bot/src/main.ts", + "apps/vscode/src/extension.ts", + ]).map((owner) => owner.id), + ).toEqual(["discord", "vscode"]); + }); +}); diff --git a/scripts/client-overlay-owner.ts b/scripts/client-overlay-owner.ts new file mode 100644 index 00000000000..912f8c2a841 --- /dev/null +++ b/scripts/client-overlay-owner.ts @@ -0,0 +1,76 @@ +#!/usr/bin/env node +// @effect-diagnostics nodeBuiltinImport:off +// @effect-diagnostics globalConsole:off + +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import * as NodeURL from "node:url"; + +export interface ClientOverlayOwnership { + readonly id: string; + readonly branch: string; + readonly pullRequest: number | null; + readonly paths: ReadonlyArray; +} + +interface ClientOverlayOwnershipManifest { + readonly overlays: ReadonlyArray; +} + +function normalizePath(value: string): string { + return value.replaceAll("\\", "/").replace(/^\.\/+/, ""); +} + +export function pathMatchesOwnershipPattern(path: string, pattern: string): boolean { + const normalizedPath = normalizePath(path); + const normalizedPattern = normalizePath(pattern); + if (normalizedPattern.endsWith("/**")) { + return normalizedPath.startsWith(normalizedPattern.slice(0, -2)); + } + return normalizedPath === normalizedPattern; +} + +export function ownersForPaths( + overlays: ReadonlyArray, + paths: ReadonlyArray, +): ReadonlyArray { + return overlays.filter((overlay) => + paths.some((path) => + overlay.paths.some((pattern) => pathMatchesOwnershipPattern(path, pattern)), + ), + ); +} + +export function readClientOverlayOwnership(sourceRoot: string): ClientOverlayOwnershipManifest { + const path = NodePath.join(sourceRoot, ".github", "client-overlay-ownership.json"); + return JSON.parse(NodeFS.readFileSync(path, "utf8")) as ClientOverlayOwnershipManifest; +} + +function main(args: ReadonlyArray): void { + if (args.length === 0) { + throw new Error("Usage: pnpm fork:overlay-owner [path...]"); + } + const sourceRoot = NodePath.resolve( + NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), + "..", + ); + const owners = ownersForPaths(readClientOverlayOwnership(sourceRoot).overlays, args); + if (owners.length === 0) { + console.log("fork/changes"); + return; + } + for (const owner of owners) { + if (owner.pullRequest === null) { + console.log(`${owner.id}: ${owner.branch} (extraction pending)`); + } else { + console.log( + `${owner.id}: PR #${owner.pullRequest} (${owner.branch}); start changes with ` + + `pnpm fork:stack overlay-start ${owner.pullRequest} `, + ); + } + } +} + +if (process.argv[1] && import.meta.url === NodeURL.pathToFileURL(process.argv[1]).href) { + main(process.argv.slice(2)); +}