Skip to content
Merged
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
4 changes: 3 additions & 1 deletion docs/specs/hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

**Must run committed Better Auth migrations before deploying code that needs them, never during a Worker request.** Postgres is reached through an uncached Hyperdrive binding. The runtime creates and closes its database pool within each request.

**Must pin locally packed core/auth packages through root pnpm overrides and commit archives, provenance, and lockfile together.** `vendor/build.json` records the source commit, dirty state, and archive hashes. No runtime import depends on a sibling checkout. The auth migrations remain owned by the package.
**Must pin locally packed core/auth packages through root pnpm overrides and commit archives, provenance, and lockfile together.** `vendor/build.json` records the source commit, archive hashes, and `dirty` — true for every `--working-tree` build, which production preflight refuses. No runtime import depends on a sibling checkout. The auth migrations remain owned by the package.

**Must declare every peer dependency of the pinned archives in `hosted/package.json`**, so they share Hosted's copy and Renovate updates them.

Source of truth: `auth` in `hosted/server/worker.ts`; `workerApp` in `hosted/server/worker-app.ts`; `migrations` in `hosted/server/migrations.ts`; `scripts/sync-pgstencil.mjs`. Pinned by `hosted/server/tests/artifacts.test.ts`.

Expand Down
23 changes: 13 additions & 10 deletions hosted/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,21 @@ does not deploy.
## Refresh private packages

```sh
node scripts/sync-pgstencil.mjs /path/to/pgstencil
node scripts/sync-pgstencil.mjs /path/to/pgstencil [revision]
```

This runs `pnpm packages:pack` in pgstencil, vendors core/auth, records source
commit/dirty state and SHA-256 hashes in `vendor/build.json`, and installs. The
direct Node command also works before the archives exist (pnpm may otherwise
auto-install first). See `docs/specs/hosted.md` -> "Application boundary" for
what has to be committed together.

Re-run integration tests after every refresh. The initial vendored pgstencil
manifest is dirty; production preflight rejects it until it is refreshed from an
accepted clean revision with matching archive hashes.
This checks out the pgstencil revision (default `HEAD`) in a temporary clean
worktree, runs `pnpm packages:pack` there, vendors core/auth, records the commit,
`dirty: false` and SHA-256 hashes in `vendor/build.json`, and installs. To try
uncommitted pgstencil changes, pass `--working-tree` instead of a revision; it
packs the checkout as it stands against its local install and always records
`dirty: true`, which production preflight rejects. The direct Node command also works before the
archives exist (pnpm may otherwise auto-install first). See
`docs/specs/hosted.md` -> "Application boundary" for what has to be committed
together.

Re-run integration tests after every refresh. Vendor an accepted pgstencil
revision before a production release.

## Resource inventory

Expand Down
1 change: 1 addition & 0 deletions hosted/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"@pgstencil/auth": "file:../vendor/pgstencil-auth-0.1.0.tgz",
"pgstencil": "file:../vendor/pgstencil-0.1.0.tgz",
"kysely": "^0.29.5",
"hono": "^4.13.8",
"@hono/node-server": "^2.0.10",
"react": "^19.2.6",
Expand Down
23 changes: 23 additions & 0 deletions hosted/server/tests/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,26 @@ test("both pinned specifiers name the recorded archives", () => {
expect(override).toBe(`file:vendor/${filename}`);
}
});

// strictPeerDependencies only rejects an out-of-range peer. pnpm resolves an
// undeclared one itself, where Renovate never sees it and Hosted's own imports
// can get a second copy.
test("Hosted declares every peer of the pinned archives", () => {
const { dependencies } = JSON.parse(
readFileSync("package.json", "utf8"),
) as { dependencies: Record<string, string> };
for (const archive of ["pgstencil-0.1.0.tgz", "pgstencil-auth-0.1.0.tgz"]) {
const manifest = execFileSync(
"tar",
["-xOf", "../vendor/" + archive, "package/package.json"],
{ encoding: "utf8" },
);
const { peerDependencies = {} } = JSON.parse(manifest) as {
peerDependencies?: Record<string, string>;
};
for (const peer of Object.keys(peerDependencies))
expect(Object.keys(dependencies), `${archive} peers on ${peer}`).toContain(
peer,
);
}
});
30 changes: 17 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ allowBuilds:
keytar: false
node-pty: true
sharp: true
# An unmet peer fails the install instead of warning, so a bump that leaves a
# shared library outside a dependent's range (pgstencil's kysely/hono, the
# @hono adapters) goes red in its own PR. Widen deliberately below, with a reason.
strictPeerDependencies: true
peerDependencyRules:
allowedVersions:
"react-helmet-async>react": ^19.0.0
Expand Down
2 changes: 1 addition & 1 deletion scripts/spec-word-budgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"docs/specs/dor-cli.md": 5900,
"docs/specs/dor-tool.md": 4100,
"docs/specs/glossary.md": 2950,
"docs/specs/hosted.md": 1050,
"docs/specs/hosted.md": 1100,
"docs/specs/layout.md": 9900,
"docs/specs/mobile-terminal-ui.md": 2000,
"docs/specs/mouse-and-clipboard.md": 3750,
Expand Down
114 changes: 78 additions & 36 deletions scripts/sync-pgstencil.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
import { resolve } from "node:path";
import {
mkdirSync,
mkdtempSync,
readFileSync,
writeFileSync,
copyFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";

const root = fileURLToPath(new URL("../", import.meta.url));
const source = process.argv[2];
if (!source)
throw new Error("Usage: pnpm pgstencil:sync /path/to/pgstencil [--packed]");
const usage =
"Usage: pnpm pgstencil:sync /path/to/pgstencil [<revision> | --working-tree]";
const [source, ...options] = process.argv.slice(2);
const revisions = options.filter((option) => !option.startsWith("--"));
const workingTree = options.includes("--working-tree");
// A mistyped flag must not fall back to a clean sync of HEAD, which would
// overwrite the archives with something other than what was asked for.
if (
!source ||
options.some(
(option) => option.startsWith("--") && option !== "--working-tree",
) ||
revisions.length > (workingTree ? 0 : 1)
)
throw new Error(usage);
const repository = resolve(source);
const run = (command, args, cwd = repository) =>
execFileSync(command, args, { cwd, stdio: "inherit" });
if (!process.argv.includes("--packed")) run("pnpm", ["packages:pack"]);
const revision = revisions[0] ?? "HEAD";
const run = (command, args, cwd, env = process.env) =>
execFileSync(command, args, { cwd, stdio: "inherit", env });
const git = (...args) =>
execFileSync("git", args, { cwd: repository, encoding: "utf8" }).trim();
const manifest = JSON.parse(
readFileSync(resolve(root, "hosted/package.json"), "utf8"),
);
Expand All @@ -28,45 +49,66 @@ const overrides = new Map(
return entry ? [[entry[1], entry[2]]] : [];
}),
);
mkdirSync(resolve(root, "vendor"), { recursive: true });
const files = [];
for (const [directory, name] of [
// Pack a committed revision in a temporary worktree after a frozen install, so
// nothing uncommitted, untracked or ignored in the pgstencil checkout (a stray
// migration, editor settings, stale build output, a local node_modules) can
// reach an archive, and build.json truthfully records `dirty: false`.
// --working-tree packs the checkout as it stands, for trying unfinished
// pgstencil changes. That result depends on local state even when git status is
// clean, so it is always recorded dirty and production preflight refuses it.
const commit = git(
"rev-parse",
"--verify",
`${workingTree ? "HEAD" : revision}^{commit}`,
);
const dirty = workingTree;
// Check the pins before the slow install and before any archive is replaced.
const read = (path) =>
workingTree
? readFileSync(resolve(repository, path), "utf8")
: git("show", `${commit}:${path}`);
const archives = [
["pgstencil", "pgstencil"],
["auth", "@pgstencil/auth"],
]) {
const pkg = JSON.parse(
readFileSync(
resolve(repository, `packages/${directory}/package.json`),
"utf8",
),
);
const filename = `${name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`;
].map(([directory, name]) => {
const { version } = JSON.parse(read(`packages/${directory}/package.json`));
const filename = `${name.replace("@", "").replace("/", "-")}-${version}.tgz`;
if (manifest.dependencies[name] !== `file:../vendor/${filename}`)
throw new Error(`Update hosted/package.json for ${filename}`);
if (overrides.get(name) !== `file:vendor/${filename}`)
throw new Error(
`Update the pnpm-workspace.yaml override for ${name} to file:vendor/${filename}`,
);
const target = resolve(root, "vendor", filename);
copyFileSync(resolve(repository, "dist/packages", filename), target);
files.push({
filename,
sha256: createHash("sha256").update(readFileSync(target)).digest("hex"),
});
return filename;
});
const checkout = workingTree
? repository
: mkdtempSync(join(tmpdir(), "pgstencil-sync-"));
// pgstencil's scripts locate their project from this variable before the cwd.
const pgstencil = { ...process.env, PGSTENCIL_PROJECT_ROOT: checkout };
if (!workingTree) git("worktree", "add", "--detach", checkout, commit);
try {
if (!workingTree)
run("pnpm", ["install", "--frozen-lockfile"], checkout, pgstencil);
run("pnpm", ["packages:pack"], checkout, pgstencil);
mkdirSync(resolve(root, "vendor"), { recursive: true });
for (const filename of archives)
copyFileSync(
resolve(checkout, "dist/packages", filename),
resolve(root, "vendor", filename),
);
} finally {
if (!workingTree) git("worktree", "remove", "--force", checkout);
}
const git = (...args) =>
execFileSync("git", args, { cwd: repository, encoding: "utf8" }).trim();
const files = archives.map((filename) => ({
filename,
sha256: createHash("sha256")
.update(readFileSync(resolve(root, "vendor", filename)))
.digest("hex"),
}));
writeFileSync(
resolve(root, "vendor/build.json"),
JSON.stringify(
{
commit: git("rev-parse", "HEAD"),
dirty: !!git("status", "--porcelain"),
files,
},
null,
2,
) + "\n",
JSON.stringify({ commit, dirty, files }, null, 2) + "\n",
);
// A changed tarball integrity is resolved by a normal install. --force also
// installs foreign-platform optional binaries and distorts dependency disclosure.
Expand Down
8 changes: 4 additions & 4 deletions vendor/build.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"commit": "c14097cabbbef8d670234354014e41ae4fc9470a",
"dirty": true,
"commit": "297edf6590e61200857b199d69160f0567bbb3c8",
"dirty": false,
"files": [
{
"filename": "pgstencil-0.1.0.tgz",
"sha256": "88601626740565b3ad8660ebdf8ec06f3d5f553288ec7ea5549f6d3e7c8a293d"
"sha256": "5ed674d0c62619a2eaa09c40ea447b730670b92ba3e32ae784755b70ed8f2c7e"
},
{
"filename": "pgstencil-auth-0.1.0.tgz",
"sha256": "111892277b8bd4a6ff5b758ccb0e1bce3997ccf5493c8f9fdb98fedf7f6eab5a"
"sha256": "7ada0c3d2031b3d06bec00c623c4f339d4045fd63b1f7a7fe371bd40ca7b7baa"
}
]
}
Binary file modified vendor/pgstencil-0.1.0.tgz
Binary file not shown.
Binary file modified vendor/pgstencil-auth-0.1.0.tgz
Binary file not shown.
Loading