feat: Ekklesia Module Package v1, the canonical packager and verifier (ADR 0049)
CI / verify (pull_request) Successful in 57s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m42s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Failing after 54s
CI / verify (pull_request) Successful in 57s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m42s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 51s
CI / deploy-smoke (split) (pull_request) Failing after 54s
A separately versioned module is now distributed as one deterministic .tar.gz and its SHA-256, produced by an Ekklesia-owned packager and checked by Core before anything is installed. This slice implements the portable `any` target, with the module template as the end-to-end proof. ADR 0049 is accepted. @ekklesia/contracts: the Module Package v1 contract. The strict ekklesia.package.json schema (packageFormat 1, identity, target, runtime, provenance, incorporated Ekklesia packages, strict transitional facts, an exact byte-wise-sorted file list with size and SHA-256), target names, and the generated runtime package.json. @ekklesia/module-pack (new, tooling): `ekklesia-pack-module --module <dir> --target any --out <dir> --source-revision <commit>`. - Bundles server, schema, and jobs with esbuild, SDK, contracts, and Drizzle included; only Node built-ins remain imports. Any warning, unresolved import, external dependency, or native or platform-specific package fails. - Copies browser artifacts as built, after proving them current, and the migrations; generates package.json and ekklesia.package.json; never ships src, tests, declarations, source maps, or workspace ranges. - THIRD_PARTY_NOTICES: every third-party package traced from the bundles' own inputs, credited once with its declared licence and every licence, copying, copyright, and notice file it ships. A missing licence fails; a missing text fails unless its standard text is complete without copyright lines (Apache-2.0, vendored, needed by drizzle-orm). - Deterministic ustar and gzip: sorted paths, uid/gid 0, empty names, mtime 0, 0755/0644, no links. Byte-identical across runs and machines. - Writes only the archive and its .sha256; never deploys, links, or installs, so the workspace's install state is untouched. Core: verifyModulePackage and `verify-module-package.js <archive> [--sha256 | --sha256-file] [--extract-to <new dir>]`. Reads the archive in memory and refuses links, special entries, setuid/setgid/sticky modes, unsafe or escaping paths, and a second top-level directory; checks metadata, every file, identity, target, Node.js range, SDK, and the transitional Drizzle agreement; runs the platform's module and migration discovery; loads the backend once in a forked process with an empty environment; only then extracts, with normalized modes, into a new directory. Errors are sanitized. @ekklesia/module-ui-build: analyzeModuleUi reports each artifact's inputs and whether the built file is current. The nested stylesheet build now works from the module directory, so artifacts no longer depend on the caller's working directory. The split deploy smoke builds the package with the canonical packager (scripts/lib/build-module-package.Dockerfile replaces the test-only pnpm deploy packaging), verifies it with the same generic image, installs the verified tree, removes it, and reinstalls the exact same archive. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
45d6506c10
commit
35ef31df9f
@@ -0,0 +1,154 @@
|
||||
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* What the packager knows about a package whose files were bundled: where it is, what it is called,
|
||||
* and whether it is third-party software. A package installed under a `node_modules` directory is
|
||||
* third-party; one that is not (a workspace package, or the module itself) is the platform's or the
|
||||
* module's own code.
|
||||
*/
|
||||
export interface BundledPackage {
|
||||
readonly root: string;
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly thirdParty: boolean;
|
||||
readonly manifest: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function readManifest(directory: string): Promise<Record<string, unknown> | undefined> {
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
await readFile(path.join(directory, "package.json"), "utf8"),
|
||||
) as unknown;
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Finds the package a bundled file belongs to: the nearest enclosing directory whose package.json
|
||||
* names a package and its version. Nested `package.json` files that only set a module type, as
|
||||
* some libraries ship in subdirectories, are skipped. */
|
||||
export class BundledPackageIndex {
|
||||
readonly #byDirectory = new Map<string, BundledPackage | undefined>();
|
||||
|
||||
async packageOf(file: string): Promise<BundledPackage | undefined> {
|
||||
let directory = path.dirname(await realpath(file).catch(() => file));
|
||||
const visited: string[] = [];
|
||||
let found: BundledPackage | undefined;
|
||||
for (;;) {
|
||||
if (this.#byDirectory.has(directory)) {
|
||||
found = this.#byDirectory.get(directory);
|
||||
break;
|
||||
}
|
||||
visited.push(directory);
|
||||
const manifest = await readManifest(directory);
|
||||
if (manifest && typeof manifest.name === "string" && typeof manifest.version === "string") {
|
||||
found = Object.freeze({
|
||||
manifest: Object.freeze(manifest),
|
||||
name: manifest.name,
|
||||
root: directory,
|
||||
thirdParty: directory.split(path.sep).includes("node_modules"),
|
||||
version: manifest.version,
|
||||
});
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(directory);
|
||||
if (parent === directory) {
|
||||
break;
|
||||
}
|
||||
directory = parent;
|
||||
}
|
||||
for (const seen of visited) {
|
||||
this.#byDirectory.set(seen, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
async function exists(file: string): Promise<boolean> {
|
||||
try {
|
||||
await access(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function declaresPlatform(manifest: Readonly<Record<string, unknown>>): boolean {
|
||||
return ["os", "cpu", "libc"].some((field) => manifest[field] !== undefined);
|
||||
}
|
||||
|
||||
async function containsCompiledAddon(directory: string): Promise<boolean> {
|
||||
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (
|
||||
entry.name !== "node_modules" &&
|
||||
(await containsCompiledAddon(path.join(directory, entry.name)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if (entry.name.toLowerCase().endsWith(".node")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The installed package a dependency name resolves to from `root`, by Node's own directory walk. */
|
||||
async function installedDependency(
|
||||
root: string,
|
||||
name: string,
|
||||
): Promise<Record<string, unknown> | undefined> {
|
||||
let directory = root;
|
||||
for (;;) {
|
||||
if (path.basename(directory) !== "node_modules") {
|
||||
const manifest = await readManifest(path.join(directory, "node_modules", ...name.split("/")));
|
||||
if (manifest) {
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
const parent = path.dirname(directory);
|
||||
if (parent === directory) {
|
||||
return undefined;
|
||||
}
|
||||
directory = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a bundled package is native or platform-specific, which an `any` package may never
|
||||
* contain (ADR 0049): it declares an operating system, CPU, or C library; it is built with node-gyp;
|
||||
* it ships a compiled `.node` addon; or one of its installed dependencies declares a platform, as
|
||||
* packages that load a per-platform binary do.
|
||||
*/
|
||||
export async function isNativeOrPlatformSpecific(bundled: BundledPackage): Promise<boolean> {
|
||||
const manifest = bundled.manifest;
|
||||
if (declaresPlatform(manifest) || manifest.gypfile === true) {
|
||||
return true;
|
||||
}
|
||||
if (await exists(path.join(bundled.root, "binding.gyp"))) {
|
||||
return true;
|
||||
}
|
||||
if (await containsCompiledAddon(bundled.root)) {
|
||||
return true;
|
||||
}
|
||||
for (const field of ["dependencies", "optionalDependencies"]) {
|
||||
const dependencies = manifest[field];
|
||||
if (!isRecord(dependencies)) {
|
||||
continue;
|
||||
}
|
||||
for (const name of Object.keys(dependencies)) {
|
||||
const dependency = await installedDependency(bundled.root, name);
|
||||
if (dependency && declaresPlatform(dependency)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user