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
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,19 @@ jobs:
restore-keys: bun-${{ runner.os }}-
- run: bun install --frozen-lockfile
- name: Stage the lockstep changelog into each package
run: for p in core rapier ws sql react convex node shell editor assets github jgengine; do cp CHANGELOG.md "packages/$p/CHANGELOG.md"; done
run: for p in core rapier ws sql navbake react convex node shell editor assets github jgengine; do cp CHANGELOG.md "packages/$p/CHANGELOG.md"; done
- name: Stage per-package skills
run: bun run stage-skills
- run: bun run check-artifacts
- run: bun run check-release-set
- run: bun run build
- run: bun run check-types:sdk
- run: bun test packages
- name: Publish unpublished packages in dependency order
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
for p in core rapier ws sql react convex node shell editor assets github jgengine; do
for p in core rapier ws sql navbake react convex node shell editor assets github jgengine; do
name=$(node -p "require('./packages/$p/package.json').name")
version=$(node -p "require('./packages/$p/package.json').version")
case "$version" in
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ between (`--json` for structured output).

### Fixed

- `@jgengine/navbake` joins the published lockstep set. `@jgengine/editor@0.18.1` depended on it while nothing published it, so `bun install` on the released SDK failed. `check-release-set` (in `gate`, `check-types`, and the publish workflow) now fails when a published package depends on a workspace package outside the publish list or out of dependency order, or when the publish, version-bump, and changelog package lists disagree.

- `pickModel` / `resolveModelPlan` warn once in dev when a `ModelPick.fallbackModel` is not in the asset catalog (a typo or an undeclared pack), instead of silently shipping a fallback that can never take over.

- Volumetric clouds no longer cut through distant buildings: the proxy dome writes per-fragment depth at the raymarch's first cloud hit, so geometry between the camera and the cloud slab occludes the clouds instead of the dome radius deciding. `STUDIO_STAGE_POST` ambient occlusion is softer (radius 1.1, intensity 1.5) so tower silhouettes stop streaking.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

## Packages

**Versions:** the lockstep game SDK set is `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}` (currently **0.18.x** — bump together). Separate cadences: CLI package `jgengine` and `@jgengine/github` (may lag; not part of that lockstep set).
**Versions:** the lockstep game SDK set is `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}` (currently **0.18.x** — bump together). Separate cadences: CLI package `jgengine` and `@jgengine/github` (may lag; not part of that lockstep set).

| Package | What it is |
| --- | --- |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"build": "bun scripts/guard.ts 600 'bun scripts/ensure-ready.ts --install-only && bun run --cwd packages/core build && bun run --cwd packages/rapier build && bun run --cwd packages/ws build && bun run --cwd packages/sql build && bun run --cwd packages/navbake build && bun run --cwd packages/react build && bun run --cwd packages/convex build && bun run --cwd packages/node build && bun run --cwd packages/shell build && bun run --cwd packages/editor build && bun run --cwd packages/assets build && bun run --cwd packages/github build && bun run --cwd packages/jgengine build && bun run build:registry'",
"deploy:cloudflare": "bun --cwd=apps/web run deploy:cloudflare",
"check-artifacts": "bun scripts/check-no-src-artifacts.ts",
"check-release-set": "bun scripts/check-release-set.ts",
"check-changelog": "bun scripts/check-changelog.ts",
"release": "bun scripts/release.ts",
"version:prerelease": "bun scripts/set-version.ts",
Expand Down
35 changes: 35 additions & 0 deletions scripts/check-release-set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";

import { checkPublishOrder, checkReleaseSet, lockstepSetFrom, publishListsFrom, versionPackagesFrom } from "./check-release-set";

describe("check-release-set", () => {
const manifests = new Map([
["core", { name: "@jgengine/core" }],
["navbake", { name: "@jgengine/navbake", dependencies: { "@jgengine/core": "^0.18.0" } }],
["editor", { name: "@jgengine/editor", dependencies: { "@jgengine/core": "^0.18.0", "@jgengine/navbake": "^0.18.0" } }],
]);

test("flags a dependency that is not published", () => {
expect(checkPublishOrder(["core", "editor"], manifests)).toEqual([
"@jgengine/editor depends on @jgengine/navbake (packages/navbake), which is not in the publish list — consumers cannot install it",
]);
});

test("flags a dependency published after its dependent", () => {
expect(checkPublishOrder(["core", "editor", "navbake"], manifests)).toEqual(["@jgengine/navbake must publish before @jgengine/editor; it is listed after"]);
});

test("passes a complete order", () => {
expect(checkPublishOrder(["core", "navbake", "editor"], manifests)).toEqual([]);
});

test("parses the three list sources", () => {
expect(publishListsFrom('for p in core navbake; do\nfor p in core navbake; do')).toEqual([["core", "navbake"], ["core", "navbake"]]);
expect(versionPackagesFrom('const PACKAGES = ["core", "navbake"];')).toEqual(["core", "navbake"]);
expect(lockstepSetFrom("`@jgengine/{core,navbake}`")).toEqual(["core", "navbake"]);
});

test("the repository's own lists agree", () => {
expect(checkReleaseSet().failures).toEqual([]);
});
});
116 changes: 116 additions & 0 deletions scripts/check-release-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Every `@jgengine/*` package a published package depends on must itself be published, and earlier
* in the publish order. `@jgengine/editor@0.18.1` shipped depending on `@jgengine/navbake`, which no
* list included, so no external consumer could install the SDK (#1752). The publish workflow, the
* version bumper, and the release changelog bullet each carry their own copy of the set; this check
* reads all three and the package manifests so they cannot drift apart again.
*/
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const root = fileURLToPath(new URL("..", import.meta.url));
const read = (rel: string) => readFileSync(`${root}${rel}`, "utf8");

export interface ReleaseSetReport {
publishOrder: string[];
failures: string[];
}

/** The `for p in …; do` package lists in the publish workflow; every list must agree. */
export function publishListsFrom(workflow: string): string[][] {
return [...workflow.matchAll(/for p in ([a-z0-9 ]+); do/g)].map((match) => match[1]!.trim().split(/\s+/));
}

/** `const PACKAGES = [...]` in set-version.ts. */
export function versionPackagesFrom(source: string): string[] {
const match = /const PACKAGES = \[([^\]]+)\]/.exec(source);
if (!match) throw new Error("set-version.ts has no `const PACKAGES = [...]`");
return [...match[1]!.matchAll(/"([a-z0-9]+)"/g)].map((entry) => entry[1]!);
}

/** The `@jgengine/{a,b,c}` set named in the release lockstep bullet. */
export function lockstepSetFrom(source: string): string[] {
const match = /@jgengine\/\{([a-z0-9,]+)\}/.exec(source);
if (!match) throw new Error("no `@jgengine/{...}` lockstep set found");
return match[1]!.split(",");
}

/**
* Checks one publish order against the manifests: every workspace dependency of a listed package is
* listed, and listed before its dependent. `manifests` maps directory name to its parsed package.json.
*/
export function checkPublishOrder(
publishOrder: readonly string[],
manifests: ReadonlyMap<string, { name: string; dependencies?: Record<string, string>; peerDependencies?: Record<string, string> }>,
): string[] {
const failures: string[] = [];
const dirByName = new Map([...manifests].map(([dir, manifest]) => [manifest.name, dir]));
const position = new Map(publishOrder.map((dir, index) => [dir, index]));
for (const dir of publishOrder) {
const manifest = manifests.get(dir);
if (manifest === undefined) {
failures.push(`publish list names "${dir}" but packages/${dir}/package.json does not exist`);
continue;
}
for (const dep of Object.keys(manifest.dependencies ?? {})) {
if (!dep.startsWith("@jgengine/")) continue;
const depDir = dirByName.get(dep);
if (depDir === undefined) {
failures.push(`${manifest.name} depends on ${dep}, which is not a workspace package`);
continue;
}
const depIndex = position.get(depDir);
if (depIndex === undefined) {
failures.push(`${manifest.name} depends on ${dep} (packages/${depDir}), which is not in the publish list — consumers cannot install it`);
} else if (depIndex > position.get(dir)!) {
failures.push(`${dep} must publish before ${manifest.name}; it is listed after`);
}
}
}
return failures;
}

export function checkReleaseSet(): ReleaseSetReport {
const failures: string[] = [];
const lists = publishListsFrom(read(".github/workflows/publish.yml"));
if (lists.length === 0) failures.push("publish.yml has no `for p in …` package list");
const publishOrder = lists[0] ?? [];
for (const list of lists.slice(1)) {
if (list.join(" ") !== publishOrder.join(" ")) failures.push(`publish.yml package lists disagree: "${publishOrder.join(" ")}" vs "${list.join(" ")}"`);
}
const versioned = versionPackagesFrom(read("scripts/set-version.ts"));
const missingFromVersion = publishOrder.filter((dir) => !versioned.includes(dir));
const extraInVersion = versioned.filter((dir) => !publishOrder.includes(dir));
if (missingFromVersion.length > 0) failures.push(`set-version.ts PACKAGES lacks published package(s): ${missingFromVersion.join(", ")}`);
if (extraInVersion.length > 0) failures.push(`set-version.ts PACKAGES bumps unpublished package(s): ${extraInVersion.join(", ")}`);

const manifests = new Map<string, { name: string; dependencies?: Record<string, string> }>();
for (const dir of readdirSync(`${root}packages`)) {
const path = `packages/${dir}/package.json`;
if (existsSync(`${root}${path}`)) manifests.set(dir, JSON.parse(read(path)));
}
failures.push(...checkPublishOrder(publishOrder, manifests));

const separateCadence = new Set(["jgengine", "github"]);
const lockstepExpected = publishOrder.filter((dir) => !separateCadence.has(dir)).sort();
for (const [label, source] of [
["scripts/release.ts", read("scripts/release.ts")],
["README.md", read("README.md")],
] as const) {
const named = lockstepSetFrom(source).sort();
if (named.join(",") !== lockstepExpected.join(",")) {
failures.push(`${label} lockstep set is {${named.join(",")}} but the publish list implies {${lockstepExpected.join(",")}}`);
}
}
return { publishOrder, failures };
}

if (import.meta.main) {
const report = checkReleaseSet();
if (report.failures.length > 0) {
console.error("check-release-set failed:");
for (const failure of report.failures) console.error(` - ${failure}`);
process.exit(1);
}
console.log(`check-release-set ok: ${report.publishOrder.length} packages publish in dependency order (${report.publishOrder.join(" ")})`);
}
2 changes: 1 addition & 1 deletion scripts/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function plainText(text: string): string {
export function lockstepBullet(sdk: string, cli: string, github: string): string {
return (
`**Bump lockstep SDK packages to \`^${sdk}\`:** ` +
`\`@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}\`. ` +
`\`@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}\`. ` +
`CLI \`jgengine\` is \`${cli}\`; \`@jgengine/github\` is \`${github}\`.`
);
}
Expand Down
1 change: 1 addition & 0 deletions scripts/run-stages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const CHECK_TYPES_STAGES: readonly Stage[] = [
{ name: "ensure-ready", ...bun("scripts/ensure-ready.ts"), rerun: "bun scripts/ensure-ready.ts" },
...[
"check-artifacts",
"check-release-set",
"check-skills",
"check-skill-api",
"check-orphan-ratchet",
Expand Down
2 changes: 1 addition & 1 deletion scripts/set-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const PACKAGES = ["core", "rapier", "ws", "sql", "react", "convex", "node", "shell", "editor", "assets", "github", "jgengine"];
const PACKAGES = ["core", "rapier", "ws", "sql", "navbake", "react", "convex", "node", "shell", "editor", "assets", "github", "jgengine"];
const root = fileURLToPath(new URL("..", import.meta.url));
const args = process.argv.slice(2);
const check = args.includes("--check");
Expand Down
Loading