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";
1212
1313import {
1414 DEVELOPMENT_ICON_OVERRIDES,
15 PUBLISH_ICON_OVERRIDES,
15 resolveWebAssetBrandForPackageVersion,
16 resolveWebIconOverrides,
1617} from "../../../scripts/lib/brand-assets.ts";
1718import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";
1819import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
......@@ -81,50 +82,34 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Stan
8182 }
8283});
8384
84interface PublishIconBackup {
85 readonly targetPath: string;
86 readonly backupPath: string;
87}
88
89const applyPublishIconOverrides = Effect.fn("applyPublishIconOverrides")(function* (
85const preparePublishIcons = Effect.fn("preparePublishIcons")(function* (
9086 repoRoot: string,
9187 serverDir: string,
88 version: string,
9289) {
9390 const path = yield* Path.Path;
9491 const fs = yield* FileSystem.FileSystem;
95 const backups: PublishIconBackup[] = [];
96
97 for (const override of PUBLISH_ICON_OVERRIDES) {
98 const sourcePath = path.join(repoRoot, override.sourceRelativePath);
99 const targetPath = path.join(serverDir, override.targetRelativePath);
100 const backupPath = `${targetPath}.publish-bak`;
101
102 if (!(yield* fs.exists(sourcePath))) {
103 return yield* new ServerCliPublishIconSourceMissingError({ sourcePath });
92 const brand = resolveWebAssetBrandForPackageVersion(version);
93 const icons = resolveWebIconOverrides(brand, "dist/client").map((override) => ({
94 sourcePath: path.join(repoRoot, override.sourceRelativePath),
95 targetPath: path.join(serverDir, override.targetRelativePath),
96 }));
97
98 for (const icon of icons) {
99 if (!(yield* fs.exists(icon.sourcePath))) {
100 return yield* new ServerCliPublishIconSourceMissingError({ sourcePath: icon.sourcePath });
104101 }
105 if (!(yield* fs.exists(targetPath))) {
106 return yield* new ServerCliPublishIconTargetMissingError({ targetPath });
102 if (!(yield* fs.exists(icon.targetPath))) {
103 return yield* new ServerCliPublishIconTargetMissingError({ targetPath: icon.targetPath });
107104 }
108
109 yield* fs.copyFile(targetPath, backupPath);
110 yield* fs.copyFile(sourcePath, targetPath);
111 backups.push({ targetPath, backupPath });
112105 }
113106
114 yield* Effect.log("[cli] Applied publish icon overrides to dist/client");
115 return backups as ReadonlyArray<PublishIconBackup>;
116});
117
118const restorePublishIconOverrides = Effect.fn("restorePublishIconOverrides")(function* (
119 backups: ReadonlyArray<PublishIconBackup>,
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 }
107 return yield* Effect.forEach(icons, (icon) =>
108 Effect.all({
109 original: fs.readFile(icon.targetPath),
110 publish: fs.readFile(icon.sourcePath),
111 }).pipe(Effect.map((contents) => ({ ...icon, ...contents }))),
112 );
128113});
129114
130115const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* (
......@@ -236,7 +221,6 @@ const publishCmd = Command.make(
236221 const repoRoot = yield* RepoRoot;
237222 const serverDir = path.join(repoRoot, "apps/server");
238223 const packageJsonPath = path.join(serverDir, "package.json");
239 const backupPath = `${packageJsonPath}.bak`;
240224
241225 // Assert build assets exist
242226 for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) {
......@@ -247,7 +231,7 @@ const publishCmd = Command.make(
247231 }
248232
249233 yield* Effect.acquireUseRelease(
250 // Acquire: backup package.json, resolve catalog dependencies, and strip devDependencies/scripts
234 // Acquire: resolve publish metadata and read every original before mutation.
251235 Effect.gen(function* () {
252236 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
253237 const workspaceConfig = yield* readWorkspaceConfig();
......@@ -273,19 +257,22 @@ const publishCmd = Command.make(
273257 ),
274258 };
275259
276 const original = yield* fs.readFileString(packageJsonPath);
277 const packageJsonString = yield* encodePackageJson(pkg);
278 yield* fs.writeFileString(backupPath, original);
279 yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`);
280 yield* Effect.log("[cli] Prepared package.json for publish");
281
282 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
283 return { iconBackups };
260 return {
261 packageJsonString: yield* encodePackageJson(pkg),
262 originalPackageJson: yield* fs.readFile(packageJsonPath),
263 icons: yield* preparePublishIcons(repoRoot, serverDir, version),
264 };
284265 }),
285266 // Use: pnpm publish from the workspace root so pnpm-only workspace
286267 // config, including override selectors, is interpreted correctly.
287 () =>
268 (resource) =>
288269 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
289276 const args = createVpPmPublishArgs(config);
290277 const spawnCommand = yield* resolveSpawnCommand("vp", ["pm", ...args]);
291278
......@@ -299,16 +286,14 @@ const publishCmd = Command.make(
299286 }),
300287 );
301288 }),
302 // Release: restore
303 (resource: { readonly iconBackups: ReadonlyArray<PublishIconBackup> }) =>
289 // Release: restore every file even if applying overrides or publishing fails.
290 (resource) =>
304291 Effect.gen(function* () {
305 yield* restorePublishIconOverrides(resource.iconBackups).pipe(
306 Effect.catch((error) =>
307 Effect.logError(`[cli] Failed to restore publish icon overrides: ${String(error)}`),
308 ),
309 );
310 yield* fs.rename(backupPath, packageJsonPath);
311 if (config.verbose) yield* Effect.log("[cli] Restored original package.json");
292 yield* fs.writeFile(packageJsonPath, resource.originalPackageJson);
293 for (const icon of resource.icons) {
294 yield* fs.writeFile(icon.targetPath, icon.original);
295 }
296 if (config.verbose) yield* Effect.log("[cli] Restored original publish assets");
312297 }),
313298 );
314299 }),
scripts/lib/brand-assets.test.ts+8-3
......@@ -4,14 +4,14 @@ import {
44 BRAND_ASSET_PATHS,
55 DEVELOPMENT_ICON_OVERRIDES,
66 DEVELOPMENT_PUBLIC_ICON_OVERRIDES,
7 PUBLISH_ICON_OVERRIDES,
87 resolveWebAssetBrandForChannel,
8 resolveWebAssetBrandForPackageVersion,
99 resolveWebIconOverrides,
1010} from "./brand-assets.ts";
1111
1212describe("brand-assets", () => {
13 it("maps server publish web assets to production icons", () => {
14 expect(PUBLISH_ICON_OVERRIDES).toEqual([
13 it("maps production web assets into the server package", () => {
14 expect(resolveWebIconOverrides("production", "dist/client")).toEqual([
1515 {
1616 sourceRelativePath: BRAND_ASSET_PATHS.productionWebFaviconIco,
1717 targetRelativePath: "dist/client/favicon.ico",
......@@ -78,6 +78,11 @@ describe("brand-assets", () => {
7878 expect(resolveWebAssetBrandForChannel("nightly")).toBe("nightly");
7979 });
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
8186 it("keeps development, nightly, and production icon families separate", () => {
8287 expect([
8388 BRAND_ASSET_PATHS.developmentIconComposerProject,
scripts/lib/brand-assets.ts+4-2
......@@ -41,6 +41,10 @@ export function resolveWebAssetBrandForChannel(channel: WebAssetChannel): WebAss
4141 return channel === "nightly" ? "nightly" : "production";
4242}
4343
44export function resolveWebAssetBrandForPackageVersion(version: string): WebAssetBrand {
45 return version.includes("-nightly.") ? "nightly" : "production";
46}
47
4448export interface IconOverride {
4549 readonly sourceRelativePath: string;
4650 readonly targetRelativePath: string;
......@@ -105,5 +109,3 @@ export const DEVELOPMENT_PUBLIC_ICON_OVERRIDES = resolveWebIconOverrides(
105109 "development",
106110 "apps/web/public",
107111);
108
109export const PUBLISH_ICON_OVERRIDES = resolveWebIconOverrides("production", "dist/client");