mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(gcp-cloud-run): include all workspace build dependencies (#2608)
Fixes #2601. Reproduced v0.7.60 from the tag: Docker frozen install failed because five workspace manifests were omitted; after adding manifests, a fresh build failed because dependency dist artifacts were absent. Changes: - Copy all workspace manifests and required source trees. - Build core, parsers, lint, SDK, sdk-playground, studio-server, and engine before producer/adapter. - Add deterministic manifest/source coverage check. Verification: - `bun run --cwd packages/gcp-cloud-run test:dockerfile-workspaces` - `docker build --progress=plain -f packages/gcp-cloud-run/Dockerfile -t hf-2601-fixed3 .` (success; image built). Source issue: https://github.com/heygen-com/hyperframes/issues/2601
This commit is contained in:
+3
-2
@@ -24,7 +24,8 @@
|
||||
"changelog:weekly": "tsx scripts/changelog-weekly.ts",
|
||||
"sync-schemas": "tsx scripts/sync-schemas.ts",
|
||||
"sync-schemas:check": "tsx scripts/sync-schemas.ts --check",
|
||||
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && oxlint . && tsx scripts/lint-skills.ts && node scripts/check-skill-mirror.mjs",
|
||||
"lint": "bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:gcp-cloud-run-dockerfile && oxlint . && tsx scripts/lint-skills.ts && node scripts/check-skill-mirror.mjs",
|
||||
"check:gcp-cloud-run-dockerfile": "bun run --cwd packages/gcp-cloud-run test:dockerfile-workspaces",
|
||||
"lint:skills": "tsx scripts/lint-skills.ts",
|
||||
"check:skill-mirror": "node scripts/check-skill-mirror.mjs",
|
||||
"lint:fix": "oxlint --fix .",
|
||||
@@ -42,7 +43,7 @@
|
||||
"player:perf": "bun run --filter @hyperframes/player perf",
|
||||
"format:check": "oxfmt --check .",
|
||||
"knip": "knip",
|
||||
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/check-workspace-contracts.test.mjs scripts/validate-release-channel.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs",
|
||||
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/check-workspace-contracts.test.mjs scripts/validate-release-channel.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs",
|
||||
"test:skills": "node --test 'skills/**/*.test.mjs'",
|
||||
"generate:previews": "tsx scripts/generate-template-previews.ts",
|
||||
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
|
||||
|
||||
@@ -92,6 +92,11 @@ COPY packages/studio/package.json packages/studio/package.json
|
||||
COPY packages/shader-transitions/package.json packages/shader-transitions/package.json
|
||||
COPY packages/aws-lambda/package.json packages/aws-lambda/package.json
|
||||
COPY packages/gcp-cloud-run/package.json packages/gcp-cloud-run/package.json
|
||||
COPY packages/lint/package.json packages/lint/package.json
|
||||
COPY packages/parsers/package.json packages/parsers/package.json
|
||||
COPY packages/sdk/package.json packages/sdk/package.json
|
||||
COPY packages/sdk-playground/package.json packages/sdk-playground/package.json
|
||||
COPY packages/studio-server/package.json packages/studio-server/package.json
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy source for the packages the render path needs.
|
||||
@@ -99,10 +104,24 @@ COPY packages/core/ packages/core/
|
||||
COPY packages/engine/ packages/engine/
|
||||
COPY packages/producer/ packages/producer/
|
||||
COPY packages/gcp-cloud-run/ packages/gcp-cloud-run/
|
||||
COPY packages/lint/ packages/lint/
|
||||
COPY packages/parsers/ packages/parsers/
|
||||
COPY packages/sdk/ packages/sdk/
|
||||
COPY packages/sdk-playground/ packages/sdk-playground/
|
||||
COPY packages/studio-server/ packages/studio-server/
|
||||
|
||||
# Build core runtime artifacts (needed by the renderer) + producer, then the
|
||||
# adapter. Generate embedded font data so glyph layout matches production.
|
||||
RUN bun run --filter @hyperframes/core build:hyperframes-runtime:modular \
|
||||
# Build workspace dependencies before the producer bundle. The published
|
||||
# workspace packages are not prebuilt in a fresh clone, so producer's esbuild
|
||||
# resolution otherwise fails on parsers/core compiler entrypoints. Generate
|
||||
# embedded font data so glyph layout matches production.
|
||||
RUN bun run --cwd packages/parsers build \
|
||||
&& bun run --cwd packages/lint build \
|
||||
&& bun run --cwd packages/studio-server build \
|
||||
&& bun run --cwd packages/core build \
|
||||
&& bun run --cwd packages/core build:hyperframes-runtime:modular \
|
||||
&& bun run --cwd packages/sdk build \
|
||||
&& bun run --cwd packages/sdk-playground build \
|
||||
&& bun run --cwd packages/engine build \
|
||||
&& (cd packages/producer && bunx tsx scripts/generate-font-data.ts) \
|
||||
&& bun run --cwd packages/producer build \
|
||||
&& bun run --cwd packages/gcp-cloud-run build
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "../..");
|
||||
const ENTRY_PACKAGE = "@hyperframes/gcp-cloud-run";
|
||||
const PRODUCER_PACKAGE = "@hyperframes/producer";
|
||||
const RUNTIME_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
|
||||
|
||||
export function readWorkspacePackages(root = ROOT) {
|
||||
return readdirSync(join(root, "packages"))
|
||||
.sort()
|
||||
.filter((directory) => existsSync(join(root, "packages", directory, "package.json")))
|
||||
.map((directory) => ({
|
||||
directory,
|
||||
manifest: JSON.parse(readFileSync(join(root, "packages", directory, "package.json"), "utf8")),
|
||||
}));
|
||||
}
|
||||
|
||||
export function findRuntimeWorkspaceDirectories(workspaces, entryPackage = ENTRY_PACKAGE) {
|
||||
const workspaceByName = new Map(
|
||||
workspaces.map((workspace) => [workspace.manifest.name, workspace]),
|
||||
);
|
||||
const visited = new Set();
|
||||
addRuntimeWorkspace(entryPackage, workspaceByName, visited);
|
||||
return new Set([...visited].map((packageName) => workspaceByName.get(packageName).directory));
|
||||
}
|
||||
|
||||
function addRuntimeWorkspace(packageName, workspaceByName, visited) {
|
||||
if (visited.has(packageName)) return;
|
||||
const workspace = workspaceByName.get(packageName);
|
||||
if (!workspace) return;
|
||||
|
||||
visited.add(packageName);
|
||||
RUNTIME_DEPENDENCY_FIELDS.flatMap((field) => Object.keys(workspace.manifest[field] ?? {}))
|
||||
.filter((dependencyName) => workspaceByName.has(dependencyName))
|
||||
.forEach((dependencyName) => addRuntimeWorkspace(dependencyName, workspaceByName, visited));
|
||||
}
|
||||
|
||||
function copiedWorkspaceDirectories(dockerfile, manifestOnly) {
|
||||
const pattern = manifestOnly
|
||||
? /^COPY packages\/([^/\s]+)\/package\.json\s+packages\/\1\/package\.json\s*$/gm
|
||||
: /^COPY packages\/([^/\s]+)\/\s+packages\/\1\/\s*$/gm;
|
||||
return new Set([...dockerfile.matchAll(pattern)].map((match) => match[1]));
|
||||
}
|
||||
|
||||
function fullBuildPositions(dockerfile) {
|
||||
const normalized = dockerfile.replace(/\\\r?\n\s*/g, " ");
|
||||
const entries = [...normalized.matchAll(/^RUN\s+(.+)$/gm)].flatMap((instruction) =>
|
||||
buildEntries(instruction[1], instruction.index),
|
||||
);
|
||||
return entries.reduce((positions, [directory, position]) => {
|
||||
const current = positions.get(directory);
|
||||
if (current === undefined || position < current) positions.set(directory, position);
|
||||
return positions;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
function buildEntries(instruction, instructionPosition) {
|
||||
const cwdEntries = [
|
||||
...instruction.matchAll(/bun run --cwd packages\/([a-z0-9-]+) build(?=\s|&&|$)/g),
|
||||
].map((match) => [match[1], instructionPosition + match.index]);
|
||||
const filterEntries = [
|
||||
...instruction.matchAll(
|
||||
/bun run --filter\s+['"]?@hyperframes\/(\{[^}]+\}|[a-z0-9-]+)['"]?\s+build(?=\s|&&|$)/g,
|
||||
),
|
||||
].flatMap((match) => {
|
||||
const directories = match[1].startsWith("{") ? match[1].slice(1, -1).split(",") : [match[1]];
|
||||
return directories.map((directory) => [directory.trim(), instructionPosition + match.index]);
|
||||
});
|
||||
return [...cwdEntries, ...filterEntries];
|
||||
}
|
||||
|
||||
export function listDockerfileWorkspaceIssues(dockerfile, workspaces) {
|
||||
const runtimeDirectories = findRuntimeWorkspaceDirectories(workspaces);
|
||||
return [
|
||||
...missingDirectoryIssues(
|
||||
"missing workspace manifests",
|
||||
workspaces.map((workspace) => workspace.directory),
|
||||
copiedWorkspaceDirectories(dockerfile, true),
|
||||
),
|
||||
...missingDirectoryIssues(
|
||||
"missing runtime workspace sources",
|
||||
runtimeDirectories,
|
||||
copiedWorkspaceDirectories(dockerfile, false),
|
||||
),
|
||||
...buildIssues(dockerfile, workspaces, runtimeDirectories),
|
||||
];
|
||||
}
|
||||
|
||||
function missingDirectoryIssues(label, requiredDirectories, presentDirectories) {
|
||||
const missing = [...requiredDirectories]
|
||||
.filter((directory) => !presentDirectories.has(directory))
|
||||
.sort();
|
||||
return missing.length > 0 ? [`${label}: ${missing.join(", ")}`] : [];
|
||||
}
|
||||
|
||||
function buildIssues(dockerfile, workspaces, runtimeDirectories) {
|
||||
const workspaceByName = new Map(
|
||||
workspaces.map((workspace) => [workspace.manifest.name, workspace]),
|
||||
);
|
||||
const workspaceByDirectory = new Map(
|
||||
workspaces.map((workspace) => [workspace.directory, workspace]),
|
||||
);
|
||||
const buildPositions = fullBuildPositions(dockerfile);
|
||||
const producerDirectory = workspaceByName.get(PRODUCER_PACKAGE)?.directory;
|
||||
const entryDirectory = workspaceByName.get(ENTRY_PACKAGE)?.directory;
|
||||
const producerPosition = buildPositions.get(producerDirectory);
|
||||
const entryPosition = buildPositions.get(entryDirectory);
|
||||
return [
|
||||
...prerequisiteBuildIssues(
|
||||
runtimeDirectories,
|
||||
workspaceByDirectory,
|
||||
buildPositions,
|
||||
producerDirectory,
|
||||
entryDirectory,
|
||||
),
|
||||
...producerBuildIssues(producerPosition),
|
||||
...entryBuildIssues(entryPosition, producerPosition),
|
||||
];
|
||||
}
|
||||
|
||||
function prerequisiteBuildIssues(
|
||||
runtimeDirectories,
|
||||
workspaceByDirectory,
|
||||
buildPositions,
|
||||
producerDirectory,
|
||||
entryDirectory,
|
||||
) {
|
||||
const producerPosition = buildPositions.get(producerDirectory);
|
||||
const missing = [...runtimeDirectories]
|
||||
.filter((directory) => directory !== producerDirectory)
|
||||
.filter((directory) => directory !== entryDirectory)
|
||||
.filter(
|
||||
(directory) =>
|
||||
typeof workspaceByDirectory.get(directory)?.manifest.scripts?.build === "string",
|
||||
)
|
||||
.filter((directory) => !isBuiltBefore(directory, producerPosition, buildPositions))
|
||||
.sort();
|
||||
return missing.length > 0
|
||||
? [`runtime workspaces must run their full build before producer: ${missing.join(", ")}`]
|
||||
: [];
|
||||
}
|
||||
|
||||
function producerBuildIssues(producerPosition) {
|
||||
return producerPosition === undefined ? ["missing full producer build"] : [];
|
||||
}
|
||||
|
||||
function isBuiltBefore(directory, laterPosition, buildPositions) {
|
||||
const position = buildPositions.get(directory);
|
||||
return position !== undefined && laterPosition !== undefined && position < laterPosition;
|
||||
}
|
||||
|
||||
function entryBuildIssues(entryPosition, producerPosition) {
|
||||
if (entryPosition === undefined) return ["missing full gcp-cloud-run build"];
|
||||
if (producerPosition !== undefined && producerPosition > entryPosition) {
|
||||
return ["producer must be built before gcp-cloud-run"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function checkDockerfileWorkspaces(root = ROOT) {
|
||||
const dockerfile = readFileSync(join(root, "packages/gcp-cloud-run/Dockerfile"), "utf8");
|
||||
const workspaces = readWorkspacePackages(root);
|
||||
const issues = listDockerfileWorkspaceIssues(dockerfile, workspaces);
|
||||
if (issues.length > 0) {
|
||||
throw new Error(`GCP Cloud Run Dockerfile workspace violations:\n- ${issues.join("\n- ")}`);
|
||||
}
|
||||
return workspaces.length;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const workspaceCount = checkDockerfileWorkspaces();
|
||||
console.log(`GCP Cloud Run Dockerfile covers ${workspaceCount} workspace manifests.`);
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
|
||||
@@ -0,0 +1,144 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
checkDockerfileWorkspaces,
|
||||
findRuntimeWorkspaceDirectories,
|
||||
listDockerfileWorkspaceIssues,
|
||||
readWorkspacePackages,
|
||||
} from "./check-dockerfile-workspaces.mjs";
|
||||
|
||||
const workspaces = [
|
||||
workspace("core", "@hyperframes/core", {
|
||||
"@hyperframes/lint": "workspace:*",
|
||||
"@hyperframes/studio-server": "workspace:*",
|
||||
}),
|
||||
workspace("engine", "@hyperframes/engine", {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
}),
|
||||
workspace("gcp-cloud-run", "@hyperframes/gcp-cloud-run", {
|
||||
"@hyperframes/producer": "workspace:*",
|
||||
}),
|
||||
workspace("lint", "@hyperframes/lint", { "@hyperframes/parsers": "workspace:*" }),
|
||||
workspace("parsers", "@hyperframes/parsers"),
|
||||
workspace("producer", "@hyperframes/producer", {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/engine": "workspace:*",
|
||||
}),
|
||||
workspace("sdk", "@hyperframes/sdk"),
|
||||
workspace("studio-server", "@hyperframes/studio-server", {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
}),
|
||||
];
|
||||
const runtimeDirectories = [
|
||||
"core",
|
||||
"engine",
|
||||
"gcp-cloud-run",
|
||||
"lint",
|
||||
"parsers",
|
||||
"producer",
|
||||
"studio-server",
|
||||
];
|
||||
const buildDirectories = [
|
||||
"core",
|
||||
"engine",
|
||||
"lint",
|
||||
"parsers",
|
||||
"studio-server",
|
||||
"producer",
|
||||
"gcp-cloud-run",
|
||||
];
|
||||
|
||||
function workspace(directory, name, dependencies = {}) {
|
||||
return { directory, manifest: { name, scripts: { build: "build" }, dependencies } };
|
||||
}
|
||||
|
||||
function dockerfile({
|
||||
manifests = workspaces.map((candidate) => candidate.directory),
|
||||
sources = runtimeDirectories,
|
||||
builds = buildDirectories,
|
||||
} = {}) {
|
||||
return [
|
||||
...manifests.map(
|
||||
(directory) => `COPY packages/${directory}/package.json packages/${directory}/package.json`,
|
||||
),
|
||||
...sources.map((directory) => `COPY packages/${directory}/ packages/${directory}/`),
|
||||
...builds.map((directory) => `RUN bun run --cwd packages/${directory} build`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
describe("GCP Cloud Run Dockerfile workspace checker", () => {
|
||||
it("accepts the repository Dockerfile", () => {
|
||||
assert.equal(checkDockerfileWorkspaces(), readWorkspacePackages().length);
|
||||
});
|
||||
|
||||
it("derives the runtime closure without looping on workspace cycles", () => {
|
||||
assert.deepEqual([...findRuntimeWorkspaceDirectories(workspaces)].sort(), runtimeDirectories);
|
||||
});
|
||||
|
||||
it("reports a missing workspace manifest", () => {
|
||||
const manifests = workspaces
|
||||
.map((candidate) => candidate.directory)
|
||||
.filter((directory) => directory !== "sdk");
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(dockerfile({ manifests }), workspaces), [
|
||||
"missing workspace manifests: sdk",
|
||||
]);
|
||||
|
||||
const misdirected = dockerfile().replace(
|
||||
"COPY packages/sdk/package.json packages/sdk/package.json",
|
||||
"COPY packages/sdk/package.json packages/wrong/package.json",
|
||||
);
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(misdirected, workspaces), [
|
||||
"missing workspace manifests: sdk",
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports a missing runtime source copy", () => {
|
||||
const sources = runtimeDirectories.filter((directory) => directory !== "parsers");
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(dockerfile({ sources }), workspaces), [
|
||||
"missing runtime workspace sources: parsers",
|
||||
]);
|
||||
|
||||
const misdirected = dockerfile().replace(
|
||||
"COPY packages/parsers/ packages/parsers/",
|
||||
"COPY packages/parsers/ packages/wrong/",
|
||||
);
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(misdirected, workspaces), [
|
||||
"missing runtime workspace sources: parsers",
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires every buildable runtime prerequisite before producer", () => {
|
||||
const builds = buildDirectories.filter((directory) => directory !== "lint");
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(dockerfile({ builds }), workspaces), [
|
||||
"runtime workspaces must run their full build before producer: lint",
|
||||
]);
|
||||
|
||||
const lateBuilds = buildDirectories.filter((directory) => directory !== "lint");
|
||||
lateBuilds.splice(lateBuilds.indexOf("producer") + 1, 0, "lint");
|
||||
assert.deepEqual(
|
||||
listDockerfileWorkspaceIssues(dockerfile({ builds: lateBuilds }), workspaces),
|
||||
["runtime workspaces must run their full build before producer: lint"],
|
||||
);
|
||||
});
|
||||
|
||||
it("does not mistake a build subcommand for a full workspace build", () => {
|
||||
const input = dockerfile()
|
||||
.replace(
|
||||
"RUN bun run --cwd packages/core build",
|
||||
"RUN bun run --cwd packages/core build:hyperframes-runtime:modular",
|
||||
)
|
||||
.concat("\n# bun run --cwd packages/core build");
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(input, workspaces), [
|
||||
"runtime workspaces must run their full build before producer: core",
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires producer to be built before the adapter", () => {
|
||||
const builds = buildDirectories.filter((directory) => directory !== "producer");
|
||||
builds.push("producer");
|
||||
assert.deepEqual(listDockerfileWorkspaceIssues(dockerfile({ builds }), workspaces), [
|
||||
"producer must be built before gcp-cloud-run",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"test:dockerfile-workspaces": "node check-dockerfile-workspaces.mjs",
|
||||
"build": "node build.mjs",
|
||||
"start": "bun dist/server.js",
|
||||
"test": "bun test",
|
||||
|
||||
Reference in New Issue
Block a user