Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/client-overlay-ownership.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
}
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ branches.
a registered overlay directly. Update its branch, or use
`pnpm fork:stack overlay-start <pr> <branch>` 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> [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 <branch>` 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`.
Expand Down
46 changes: 46 additions & 0 deletions docs/client-overlays.md
Original file line number Diff line number Diff line change
@@ -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> [changed-path...]
```

- `fork/changes` means no extracted client owns the path.
- A PR number means start a child with
`pnpm fork:stack overlay-start <pr-number> <branch>` 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.
5 changes: 5 additions & 0 deletions docs/fork-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
49 changes: 49 additions & 0 deletions scripts/client-overlay-owner.test.ts
Original file line number Diff line number Diff line change
@@ -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<ClientOverlayOwnership> = [
{
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"]);
});
});
76 changes: 76 additions & 0 deletions scripts/client-overlay-owner.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
}

interface ClientOverlayOwnershipManifest {
readonly overlays: ReadonlyArray<ClientOverlayOwnership>;
}

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<ClientOverlayOwnership>,
paths: ReadonlyArray<string>,
): ReadonlyArray<ClientOverlayOwnership> {
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<string>): void {
if (args.length === 0) {
throw new Error("Usage: pnpm fork:overlay-owner <path> [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} <branch>`,
);
}
}
}

if (process.argv[1] && import.meta.url === NodeURL.pathToFileURL(process.argv[1]).href) {
main(process.argv.slice(2));
}
Loading