authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-07-23 17:36:02-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 23:36:02+02:00
logedb12401e547be4806836077f336295c58097c9b
treef08692543730d96205425f326083c4dc87333dee
parent79fe11bc1c586029dfe052df25f4b2d6227b3b33
signature Signed by PGP key B5690EEEBB952194

fix(cli): publish nightly branded favicons (#4372)


3 files changed, 53 insertions(+), 61 deletions(-)

apps/server/scripts/cli.ts+41-56
...@@ -12,7 +12,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";...@@ -12,7 +12,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1212
13import {13import {
14 DEVELOPMENT_ICON_OVERRIDES,14 DEVELOPMENT_ICON_OVERRIDES,
15 PUBLISH_ICON_OVERRIDES,15 resolveWebAssetBrandForPackageVersion,
16 resolveWebIconOverrides,
16} from "../../../scripts/lib/brand-assets.ts";17} from "../../../scripts/lib/brand-assets.ts";
17import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";18import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";
18import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";19import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
...@@ -81,50 +82,34 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Stan...@@ -81,50 +82,34 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Stan
81 }82 }
82});83});
8384
84interface PublishIconBackup {85const preparePublishIcons = Effect.fn("preparePublishIcons")(function* (
85 readonly targetPath: string;
86 readonly backupPath: string;
87}
88
89const applyPublishIconOverrides = Effect.fn("applyPublishIconOverrides")(function* (
90 repoRoot: string,86 repoRoot: string,
91 serverDir: string,87 serverDir: string,
88 version: string,
92) {89) {
93 const path = yield* Path.Path;90 const path = yield* Path.Path;
94 const fs = yield* FileSystem.FileSystem;91 const fs = yield* FileSystem.FileSystem;
95 const backups: PublishIconBackup[] = [];92 const brand = resolveWebAssetBrandForPackageVersion(version);
9693 const icons = resolveWebIconOverrides(brand, "dist/client").map((override) => ({
97 for (const override of PUBLISH_ICON_OVERRIDES) {94 sourcePath: path.join(repoRoot, override.sourceRelativePath),
98 const sourcePath = path.join(repoRoot, override.sourceRelativePath);95 targetPath: path.join(serverDir, override.targetRelativePath),
99 const targetPath = path.join(serverDir, override.targetRelativePath);96 }));
100 const backupPath = `${targetPath}.publish-bak`;97
10198 for (const icon of icons) {
102 if (!(yield* fs.exists(sourcePath))) {99 if (!(yield* fs.exists(icon.sourcePath))) {
103 return yield* new ServerCliPublishIconSourceMissingError({ sourcePath });100 return yield* new ServerCliPublishIconSourceMissingError({ sourcePath: icon.sourcePath });
104 }101 }
105 if (!(yield* fs.exists(targetPath))) {102 if (!(yield* fs.exists(icon.targetPath))) {
106 return yield* new ServerCliPublishIconTargetMissingError({ targetPath });103 return yield* new ServerCliPublishIconTargetMissingError({ targetPath: icon.targetPath });
107 }104 }
108
109 yield* fs.copyFile(targetPath, backupPath);
110 yield* fs.copyFile(sourcePath, targetPath);
111 backups.push({ targetPath, backupPath });
112 }105 }
113106
114 yield* Effect.log("[cli] Applied publish icon overrides to dist/client");107 return yield* Effect.forEach(icons, (icon) =>
115 return backups as ReadonlyArray<PublishIconBackup>;108 Effect.all({
116});109 original: fs.readFile(icon.targetPath),
117110 publish: fs.readFile(icon.sourcePath),
118const restorePublishIconOverrides = Effect.fn("restorePublishIconOverrides")(function* (111 }).pipe(Effect.map((contents) => ({ ...icon, ...contents }))),
119 backups: ReadonlyArray<PublishIconBackup>,112 );
120) {
121 const fs = yield* FileSystem.FileSystem;
122 for (const backup of backups) {
123 if (!(yield* fs.exists(backup.backupPath))) {
124 continue;
125 }
126 yield* fs.rename(backup.backupPath, backup.targetPath);
127 }
128});113});
129114
130const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* (115const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* (
...@@ -236,7 +221,6 @@ const publishCmd = Command.make(...@@ -236,7 +221,6 @@ const publishCmd = Command.make(
236 const repoRoot = yield* RepoRoot;221 const repoRoot = yield* RepoRoot;
237 const serverDir = path.join(repoRoot, "apps/server");222 const serverDir = path.join(repoRoot, "apps/server");
238 const packageJsonPath = path.join(serverDir, "package.json");223 const packageJsonPath = path.join(serverDir, "package.json");
239 const backupPath = `${packageJsonPath}.bak`;
240224
241 // Assert build assets exist225 // Assert build assets exist
242 for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) {226 for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) {
...@@ -247,7 +231,7 @@ const publishCmd = Command.make(...@@ -247,7 +231,7 @@ const publishCmd = Command.make(
247 }231 }
248232
249 yield* Effect.acquireUseRelease(233 yield* Effect.acquireUseRelease(
250 // Acquire: backup package.json, resolve catalog dependencies, and strip devDependencies/scripts234 // Acquire: resolve publish metadata and read every original before mutation.
251 Effect.gen(function* () {235 Effect.gen(function* () {
252 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);236 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
253 const workspaceConfig = yield* readWorkspaceConfig();237 const workspaceConfig = yield* readWorkspaceConfig();
...@@ -273,19 +257,22 @@ const publishCmd = Command.make(...@@ -273,19 +257,22 @@ const publishCmd = Command.make(
273 ),257 ),
274 };258 };
275259
276 const original = yield* fs.readFileString(packageJsonPath);260 return {
277 const packageJsonString = yield* encodePackageJson(pkg);261 packageJsonString: yield* encodePackageJson(pkg),
278 yield* fs.writeFileString(backupPath, original);262 originalPackageJson: yield* fs.readFile(packageJsonPath),
279 yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`);263 icons: yield* preparePublishIcons(repoRoot, serverDir, version),
280 yield* Effect.log("[cli] Prepared package.json for publish");264 };
281
282 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
283 return { iconBackups };
284 }),265 }),
285 // Use: pnpm publish from the workspace root so pnpm-only workspace266 // Use: pnpm publish from the workspace root so pnpm-only workspace
286 // config, including override selectors, is interpreted correctly.267 // config, including override selectors, is interpreted correctly.
287 () =>268 (resource) =>
288 Effect.gen(function* () {269 Effect.gen(function* () {
270 yield* fs.writeFileString(packageJsonPath, `${resource.packageJsonString}\n`);
271 for (const icon of resource.icons) {
272 yield* fs.writeFile(icon.targetPath, icon.publish);
273 }
274 yield* Effect.log("[cli] Applied package metadata and publish icon overrides");
275
289 const args = createVpPmPublishArgs(config);276 const args = createVpPmPublishArgs(config);
290 const spawnCommand = yield* resolveSpawnCommand("vp", ["pm", ...args]);277 const spawnCommand = yield* resolveSpawnCommand("vp", ["pm", ...args]);
291278
...@@ -299,16 +286,14 @@ const publishCmd = Command.make(...@@ -299,16 +286,14 @@ const publishCmd = Command.make(
299 }),286 }),
300 );287 );
301 }),288 }),
302 // Release: restore289 // Release: restore every file even if applying overrides or publishing fails.
303 (resource: { readonly iconBackups: ReadonlyArray<PublishIconBackup> }) =>290 (resource) =>
304 Effect.gen(function* () {291 Effect.gen(function* () {
305 yield* restorePublishIconOverrides(resource.iconBackups).pipe(292 yield* fs.writeFile(packageJsonPath, resource.originalPackageJson);
306 Effect.catch((error) =>293 for (const icon of resource.icons) {
307 Effect.logError(`[cli] Failed to restore publish icon overrides: ${String(error)}`),294 yield* fs.writeFile(icon.targetPath, icon.original);
308 ),295 }
309 );296 if (config.verbose) yield* Effect.log("[cli] Restored original publish assets");
310 yield* fs.rename(backupPath, packageJsonPath);
311 if (config.verbose) yield* Effect.log("[cli] Restored original package.json");
312 }),297 }),
313 );298 );
314 }),299 }),
scripts/lib/brand-assets.test.ts+8-3
...@@ -4,14 +4,14 @@ import {...@@ -4,14 +4,14 @@ import {
4 BRAND_ASSET_PATHS,4 BRAND_ASSET_PATHS,
5 DEVELOPMENT_ICON_OVERRIDES,5 DEVELOPMENT_ICON_OVERRIDES,
6 DEVELOPMENT_PUBLIC_ICON_OVERRIDES,6 DEVELOPMENT_PUBLIC_ICON_OVERRIDES,
7 PUBLISH_ICON_OVERRIDES,
8 resolveWebAssetBrandForChannel,7 resolveWebAssetBrandForChannel,
8 resolveWebAssetBrandForPackageVersion,
9 resolveWebIconOverrides,9 resolveWebIconOverrides,
10} from "./brand-assets.ts";10} from "./brand-assets.ts";
1111
12describe("brand-assets", () => {12describe("brand-assets", () => {
13 it("maps server publish web assets to production icons", () => {13 it("maps production web assets into the server package", () => {
14 expect(PUBLISH_ICON_OVERRIDES).toEqual([14 expect(resolveWebIconOverrides("production", "dist/client")).toEqual([
15 {15 {
16 sourceRelativePath: BRAND_ASSET_PATHS.productionWebFaviconIco,16 sourceRelativePath: BRAND_ASSET_PATHS.productionWebFaviconIco,
17 targetRelativePath: "dist/client/favicon.ico",17 targetRelativePath: "dist/client/favicon.ico",
...@@ -78,6 +78,11 @@ describe("brand-assets", () => {...@@ -78,6 +78,11 @@ describe("brand-assets", () => {
78 expect(resolveWebAssetBrandForChannel("nightly")).toBe("nightly");78 expect(resolveWebAssetBrandForChannel("nightly")).toBe("nightly");
79 });79 });
8080
81 it("maps package versions to web asset brands", () => {
82 expect(resolveWebAssetBrandForPackageVersion("0.0.29")).toBe("production");
83 expect(resolveWebAssetBrandForPackageVersion("0.0.29-nightly.20260723.882")).toBe("nightly");
84 });
85
81 it("keeps development, nightly, and production icon families separate", () => {86 it("keeps development, nightly, and production icon families separate", () => {
82 expect([87 expect([
83 BRAND_ASSET_PATHS.developmentIconComposerProject,88 BRAND_ASSET_PATHS.developmentIconComposerProject,
scripts/lib/brand-assets.ts+4-2
...@@ -41,6 +41,10 @@ export function resolveWebAssetBrandForChannel(channel: WebAssetChannel): WebAss...@@ -41,6 +41,10 @@ export function resolveWebAssetBrandForChannel(channel: WebAssetChannel): WebAss
41 return channel === "nightly" ? "nightly" : "production";41 return channel === "nightly" ? "nightly" : "production";
42}42}
4343
44export function resolveWebAssetBrandForPackageVersion(version: string): WebAssetBrand {
45 return version.includes("-nightly.") ? "nightly" : "production";
46}
47
44export interface IconOverride {48export interface IconOverride {
45 readonly sourceRelativePath: string;49 readonly sourceRelativePath: string;
46 readonly targetRelativePath: string;50 readonly targetRelativePath: string;
...@@ -105,5 +109,3 @@ export const DEVELOPMENT_PUBLIC_ICON_OVERRIDES = resolveWebIconOverrides(...@@ -105,5 +109,3 @@ export const DEVELOPMENT_PUBLIC_ICON_OVERRIDES = resolveWebIconOverrides(
105 "development",109 "development",
106 "apps/web/public",110 "apps/web/public",
107);111);
108
109export const PUBLISH_ICON_OVERRIDES = resolveWebIconOverrides("production", "dist/client");