Showing the changes to scripts (7 of 49 files). Show all
Unified · Split
| @@ -2,9 +2,59 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; · 1 unmodified line | |||
| 2 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import { ConfigProvider, Effect, Option } from "effect"; | 3 | import { ConfigProvider, Effect, Option } from "effect"; |
| 4 | 4 | ||
| 5 | import { resolveBuildOptions } from "./build-desktop-artifact.ts"; | 5 | import { |
| 6 | resolveBuildOptions, | ||
| 7 | resolveDesktopBuildIconAssets, | ||
| 8 | resolveDesktopUpdateChannel, | ||
| 9 | resolveMockUpdateServerPort, | ||
| 10 | resolveMockUpdateServerUrl, | ||
| 11 | } from "./build-desktop-artifact.ts"; | ||
| 12 | import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; | ||
| 6 | 13 | ||
| 7 | it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { | 14 | it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 15 | it("resolves the dedicated nightly updater channel from nightly versions", () => { | ||
| 16 | assert.equal(resolveDesktopUpdateChannel("0.0.17-nightly.20260413.42"), "nightly"); | ||
| 17 | assert.equal(resolveDesktopUpdateChannel("0.0.17"), "latest"); | ||
| 18 | }); | ||
| 19 | |||
| 20 | it("switches desktop packaging icons to the nightly artwork for nightly versions", () => { | ||
| 21 | assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17"), { | ||
| 22 | macIconPng: BRAND_ASSET_PATHS.productionMacIconPng, | ||
| 23 | linuxIconPng: BRAND_ASSET_PATHS.productionLinuxIconPng, | ||
| 24 | windowsIconIco: BRAND_ASSET_PATHS.productionWindowsIconIco, | ||
| 25 | }); | ||
| 26 | |||
| 27 | assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17-nightly.20260413.42"), { | ||
| 28 | macIconPng: BRAND_ASSET_PATHS.nightlyMacIconPng, | ||
| 29 | linuxIconPng: BRAND_ASSET_PATHS.nightlyLinuxIconPng, | ||
| 30 | windowsIconIco: BRAND_ASSET_PATHS.nightlyWindowsIconIco, | ||
| 31 | }); | ||
| 32 | }); | ||
| 33 | |||
| 34 | it("falls back to the default mock update port when the configured port is blank", () => { | ||
| 35 | assert.equal(resolveMockUpdateServerUrl(undefined), "http://localhost:3000"); | ||
| 36 | assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123"); | ||
| 37 | }); | ||
| 38 | |||
| 39 | it.effect("normalizes mock update server ports from env-style strings", () => | ||
| 40 | Effect.gen(function* () { | ||
| 41 | assert.equal(yield* resolveMockUpdateServerPort(undefined), undefined); | ||
| 42 | assert.equal(yield* resolveMockUpdateServerPort(""), undefined); | ||
| 43 | assert.equal(yield* resolveMockUpdateServerPort(" "), undefined); | ||
| 44 | assert.equal(yield* resolveMockUpdateServerPort("4123"), 4123); | ||
| 45 | }), | ||
| 46 | ); | ||
| 47 | |||
| 48 | it.effect("rejects non-numeric or out-of-range mock update ports", () => | ||
| 49 | Effect.gen(function* () { | ||
| 50 | const invalidPorts = ["abc", "12.5", "0", "65536"]; | ||
| 51 | for (const port of invalidPorts) { | ||
| 52 | const exit = yield* Effect.exit(resolveMockUpdateServerPort(port)); | ||
| 53 | assert.equal(exit._tag, "Failure"); | ||
| 54 | } | ||
| 55 | }), | ||
| 56 | ); | ||
| 57 | |||
| 8 | it.effect("preserves explicit false boolean flags over true env defaults", () => | 58 | it.effect("preserves explicit false boolean flags over true env defaults", () => |
| 9 | Effect.gen(function* () { | 59 | Effect.gen(function* () { |
| 10 | const resolved = yield* resolveBuildOptions({ | 60 | const resolved = yield* resolveBuildOptions({ |
| @@ -23,23 +23,14 @@ const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); · 22 unmodified lines | |||
| 23 | const RepoRoot = Effect.service(Path.Path).pipe( | 23 | const RepoRoot = Effect.service(Path.Path).pipe( |
| 24 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), | 24 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), |
| 25 | ); | 25 | ); |
| 26 | const ProductionMacIconSource = Effect.zipWith( | ||
| 27 | RepoRoot, | ||
| 28 | Effect.service(Path.Path), | ||
| 29 | (repoRoot, path) => path.join(repoRoot, BRAND_ASSET_PATHS.productionMacIconPng), | ||
| 30 | ); | ||
| 31 | const ProductionLinuxIconSource = Effect.zipWith( | ||
| 32 | RepoRoot, | ||
| 33 | Effect.service(Path.Path), | ||
| 34 | (repoRoot, path) => path.join(repoRoot, BRAND_ASSET_PATHS.productionLinuxIconPng), | ||
| 35 | ); | ||
| 36 | const ProductionWindowsIconSource = Effect.zipWith( | ||
| 37 | RepoRoot, | ||
| 38 | Effect.service(Path.Path), | ||
| 39 | (repoRoot, path) => path.join(repoRoot, BRAND_ASSET_PATHS.productionWindowsIconIco), | ||
| 40 | ); | ||
| 41 | const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); | 26 | const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); |
| 42 | 27 | ||
| 28 | interface DesktopBuildIconAssets { | ||
| 29 | readonly macIconPng: string; | ||
| 30 | readonly linuxIconPng: string; | ||
| 31 | readonly windowsIconIco: string; | ||
| 32 | } | ||
| 33 | |||
| 43 | interface PlatformConfig { | 34 | interface PlatformConfig { |
| 44 | readonly cliFlag: "--mac" | "--linux" | "--win"; | 35 | readonly cliFlag: "--mac" | "--linux" | "--win"; |
| 45 | readonly defaultTarget: string; | 36 | readonly defaultTarget: string; |
| @@ -75,7 +66,7 @@ interface BuildCliInput { · 29 unmodified lines | |||
| 75 | readonly signed: Option.Option<boolean>; | 66 | readonly signed: Option.Option<boolean>; |
| 76 | readonly verbose: Option.Option<boolean>; | 67 | readonly verbose: Option.Option<boolean>; |
| 77 | readonly mockUpdates: Option.Option<boolean>; | 68 | readonly mockUpdates: Option.Option<boolean>; |
| 78 | readonly mockUpdateServerPort: Option.Option<string>; | 69 | readonly mockUpdateServerPort: Option.Option<number>; |
| 79 | } | 70 | } |
| 80 | 71 | ||
| 81 | function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { | 72 | function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { |
| @@ -165,7 +156,7 @@ interface ResolvedBuildOptions { · 83 unmodified lines | |||
| 165 | readonly signed: boolean; | 156 | readonly signed: boolean; |
| 166 | readonly verbose: boolean; | 157 | readonly verbose: boolean; |
| 167 | readonly mockUpdates: boolean; | 158 | readonly mockUpdates: boolean; |
| 168 | readonly mockUpdateServerPort: string | undefined; | 159 | readonly mockUpdateServerPort: number | undefined; |
| 169 | } | 160 | } |
| 170 | 161 | ||
| 171 | interface StagePackageJson { | 162 | interface StagePackageJson { |
| @@ -213,11 +204,28 @@ const BuildEnvConfig = Config.all({ · 41 unmodified lines | |||
| 213 | mockUpdateServerPort: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.option), | 204 | mockUpdateServerPort: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.option), |
| 214 | }); | 205 | }); |
| 215 | 206 | ||
| 207 | const MockUpdateServerPortSchema = Schema.NumberFromString.check( | ||
| 208 | Schema.isInt(), | ||
| 209 | Schema.isBetween({ minimum: 1, maximum: 65535 }), | ||
| 210 | ); | ||
| 211 | const decodeMockUpdateServerPort = Schema.decodeUnknownEffect(MockUpdateServerPortSchema); | ||
| 212 | |||
| 216 | const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => | 213 | const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => |
| 217 | Option.getOrElse(flag, () => envValue); | 214 | Option.getOrElse(flag, () => envValue); |
| 218 | const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) => | 215 | const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) => |
| 219 | Option.getOrElse(a, () => Option.getOrElse(b, () => defaultValue)); | 216 | Option.getOrElse(a, () => Option.getOrElse(b, () => defaultValue)); |
| 220 | 217 | ||
| 218 | export const resolveMockUpdateServerPort = Effect.fn("resolveMockUpdateServerPort")(function* ( | ||
| 219 | mockUpdateServerPort: string | undefined, | ||
| 220 | ) { | ||
| 221 | const port = mockUpdateServerPort?.trim(); | ||
| 222 | if (!port) { | ||
| 223 | return undefined; | ||
| 224 | } | ||
| 225 | |||
| 226 | return yield* decodeMockUpdateServerPort(port); | ||
| 227 | }); | ||
| 228 | |||
| 221 | export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( | 229 | export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( |
| 222 | input: BuildCliInput, | 230 | input: BuildCliInput, |
| 223 | ) { | 231 | ) { |
| @@ -254,11 +262,17 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( · 30 unmodified lines | |||
| 254 | const verbose = resolveBooleanFlag(input.verbose, env.verbose); | 262 | const verbose = resolveBooleanFlag(input.verbose, env.verbose); |
| 255 | 263 | ||
| 256 | const mockUpdates = resolveBooleanFlag(input.mockUpdates, env.mockUpdates); | 264 | const mockUpdates = resolveBooleanFlag(input.mockUpdates, env.mockUpdates); |
| 257 | const mockUpdateServerPort = mergeOptions( | 265 | const mockUpdateServerPort = |
| 258 | input.mockUpdateServerPort, | 266 | Option.getOrUndefined(input.mockUpdateServerPort) ?? |
| 259 | env.mockUpdateServerPort, | 267 | (yield* resolveMockUpdateServerPort(Option.getOrUndefined(env.mockUpdateServerPort)).pipe( |
| 260 | undefined, | 268 | Effect.mapError( |
| 261 | ); | 269 | (cause) => |
| 270 | new BuildScriptError({ | ||
| 271 | message: "Invalid mock update server port.", | ||
| 272 | cause, | ||
| 273 | }), | ||
| 274 | ), | ||
| 275 | )); | ||
| 262 | 276 | ||
| 263 | return { | 277 | return { |
| 264 | platform, | 278 | platform, |
| @@ -329,14 +343,13 @@ function generateMacIconSet( · 64 unmodified lines | |||
| 329 | }); | 343 | }); |
| 330 | } | 344 | } |
| 331 | 345 | ||
| 332 | function stageMacIcons(stageResourcesDir: string, verbose: boolean) { | 346 | function stageMacIcons(stageResourcesDir: string, sourcePng: string, verbose: boolean) { |
| 333 | return Effect.gen(function* () { | 347 | return Effect.gen(function* () { |
| 334 | const fs = yield* FileSystem.FileSystem; | 348 | const fs = yield* FileSystem.FileSystem; |
| 335 | const path = yield* Path.Path; | 349 | const path = yield* Path.Path; |
| 336 | const iconSource = yield* ProductionMacIconSource; | 350 | if (!(yield* fs.exists(sourcePng))) { |
| 337 | if (!(yield* fs.exists(iconSource))) { | ||
| 338 | return yield* new BuildScriptError({ | 351 | return yield* new BuildScriptError({ |
| 339 | message: `Production icon source is missing at ${iconSource}`, | 352 | message: `Desktop macOS icon source is missing at ${sourcePng}`, |
| 340 | }); | 353 | }); |
| 341 | } | 354 | } |
| 342 | 355 | ||
| @@ -350,42 +363,40 @@ function stageMacIcons(stageResourcesDir: string, verbose: boolean) { · 7 unmodified lines | |||
| 350 | yield* runCommand( | 363 | yield* runCommand( |
| 351 | ChildProcess.make({ | 364 | ChildProcess.make({ |
| 352 | ...commandOutputOptions(verbose), | 365 | ...commandOutputOptions(verbose), |
| 353 | })`sips -z 512 512 ${iconSource} --out ${iconPngPath}`, | 366 | })`sips -z 512 512 ${sourcePng} --out ${iconPngPath}`, |
| 354 | ); | 367 | ); |
| 355 | 368 | ||
| 356 | yield* generateMacIconSet(iconSource, iconIcnsPath, tmpRoot, path, verbose); | 369 | yield* generateMacIconSet(sourcePng, iconIcnsPath, tmpRoot, path, verbose); |
| 357 | }); | 370 | }); |
| 358 | } | 371 | } |
| 359 | 372 | ||
| 360 | function stageLinuxIcons(stageResourcesDir: string) { | 373 | function stageLinuxIcons(stageResourcesDir: string, sourcePng: string) { |
| 361 | return Effect.gen(function* () { | 374 | return Effect.gen(function* () { |
| 362 | const fs = yield* FileSystem.FileSystem; | 375 | const fs = yield* FileSystem.FileSystem; |
| 363 | const path = yield* Path.Path; | 376 | const path = yield* Path.Path; |
| 364 | const iconSource = yield* ProductionLinuxIconSource; | 377 | if (!(yield* fs.exists(sourcePng))) { |
| 365 | if (!(yield* fs.exists(iconSource))) { | ||
| 366 | return yield* new BuildScriptError({ | 378 | return yield* new BuildScriptError({ |
| 367 | message: `Production icon source is missing at ${iconSource}`, | 379 | message: `Desktop Linux icon source is missing at ${sourcePng}`, |
| 368 | }); | 380 | }); |
| 369 | } | 381 | } |
| 370 | 382 | ||
| 371 | const iconPath = path.join(stageResourcesDir, "icon.png"); | 383 | const iconPath = path.join(stageResourcesDir, "icon.png"); |
| 372 | yield* fs.copyFile(iconSource, iconPath); | 384 | yield* fs.copyFile(sourcePng, iconPath); |
| 373 | }); | 385 | }); |
| 374 | } | 386 | } |
| 375 | 387 | ||
| 376 | function stageWindowsIcons(stageResourcesDir: string) { | 388 | function stageWindowsIcons(stageResourcesDir: string, sourceIco: string) { |
| 377 | return Effect.gen(function* () { | 389 | return Effect.gen(function* () { |
| 378 | const fs = yield* FileSystem.FileSystem; | 390 | const fs = yield* FileSystem.FileSystem; |
| 379 | const path = yield* Path.Path; | 391 | const path = yield* Path.Path; |
| 380 | const iconSource = yield* ProductionWindowsIconSource; | 392 | if (!(yield* fs.exists(sourceIco))) { |
| 381 | if (!(yield* fs.exists(iconSource))) { | ||
| 382 | return yield* new BuildScriptError({ | 393 | return yield* new BuildScriptError({ |
| 383 | message: `Production Windows icon source is missing at ${iconSource}`, | 394 | message: `Desktop Windows icon source is missing at ${sourceIco}`, |
| 384 | }); | 395 | }); |
| 385 | } | 396 | } |
| 386 | 397 | ||
| 387 | const iconPath = path.join(stageResourcesDir, "icon.ico"); | 398 | const iconPath = path.join(stageResourcesDir, "icon.ico"); |
| 388 | yield* fs.copyFile(iconSource, iconPath); | 399 | yield* fs.copyFile(sourceIco, iconPath); |
| 389 | }); | 400 | }); |
| 390 | } | 401 | } |
| 391 | 402 | ||
| @@ -441,12 +452,13 @@ function resolveDesktopRuntimeDependencies( · 49 unmodified lines | |||
| 441 | return resolveCatalogDependencies(runtimeDependencies, catalog, "apps/desktop"); | 452 | return resolveCatalogDependencies(runtimeDependencies, catalog, "apps/desktop"); |
| 442 | } | 453 | } |
| 443 | 454 | ||
| 444 | function resolveGitHubPublishConfig(): | 455 | function resolveGitHubPublishConfig(updateChannel: "latest" | "nightly"): |
| 445 | | { | 456 | | { |
| 446 | readonly provider: "github"; | 457 | readonly provider: "github"; |
| 447 | readonly owner: string; | 458 | readonly owner: string; |
| 448 | readonly repo: string; | 459 | readonly repo: string; |
| 449 | readonly releaseType: "release"; | 460 | readonly releaseType: "release" | "prerelease"; |
| 461 | readonly channel?: "nightly"; | ||
| 450 | } | 462 | } |
| 451 | | undefined { | 463 | | undefined { |
| 452 | const rawRepo = | 464 | const rawRepo = |
| @@ -462,17 +474,43 @@ function resolveGitHubPublishConfig(): · 9 unmodified lines | |||
| 462 | provider: "github", | 474 | provider: "github", |
| 463 | owner, | 475 | owner, |
| 464 | repo, | 476 | repo, |
| 465 | releaseType: "release", | 477 | releaseType: updateChannel === "nightly" ? "prerelease" : "release", |
| 478 | ...(updateChannel === "nightly" ? { channel: "nightly" as const } : {}), | ||
| 466 | }; | 479 | }; |
| 467 | } | 480 | } |
| 468 | 481 | ||
| 482 | export function resolveDesktopUpdateChannel(version: string): "latest" | "nightly" { | ||
| 483 | return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest"; | ||
| 484 | } | ||
| 485 | |||
| 486 | export function resolveDesktopBuildIconAssets(version: string): DesktopBuildIconAssets { | ||
| 487 | if (resolveDesktopUpdateChannel(version) === "nightly") { | ||
| 488 | return { | ||
| 489 | macIconPng: BRAND_ASSET_PATHS.nightlyMacIconPng, | ||
| 490 | linuxIconPng: BRAND_ASSET_PATHS.nightlyLinuxIconPng, | ||
| 491 | windowsIconIco: BRAND_ASSET_PATHS.nightlyWindowsIconIco, | ||
| 492 | }; | ||
| 493 | } | ||
| 494 | |||
| 495 | return { | ||
| 496 | macIconPng: BRAND_ASSET_PATHS.productionMacIconPng, | ||
| 497 | linuxIconPng: BRAND_ASSET_PATHS.productionLinuxIconPng, | ||
| 498 | windowsIconIco: BRAND_ASSET_PATHS.productionWindowsIconIco, | ||
| 499 | }; | ||
| 500 | } | ||
| 501 | |||
| 502 | export function resolveMockUpdateServerUrl(mockUpdateServerPort: number | undefined): string { | ||
| 503 | return `http://localhost:${mockUpdateServerPort ?? 3000}`; | ||
| 504 | } | ||
| 505 | |||
| 469 | const createBuildConfig = Effect.fn("createBuildConfig")(function* ( | 506 | const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 470 | platform: typeof BuildPlatform.Type, | 507 | platform: typeof BuildPlatform.Type, |
| 471 | target: string, | 508 | target: string, |
| 472 | productName: string, | 509 | productName: string, |
| 510 | version: string, | ||
| 473 | signed: boolean, | 511 | signed: boolean, |
| 474 | mockUpdates: boolean, | 512 | mockUpdates: boolean, |
| 475 | mockUpdateServerPort: string | undefined, | 513 | mockUpdateServerPort: number | undefined, |
| 476 | ) { | 514 | ) { |
| 477 | const buildConfig: Record<string, unknown> = { | 515 | const buildConfig: Record<string, unknown> = { |
| 478 | appId: "com.t3tools.t3code", | 516 | appId: "com.t3tools.t3code", |
| @@ -482,14 +520,15 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( · 3 unmodified lines | |||
| 482 | buildResources: "apps/desktop/resources", | 520 | buildResources: "apps/desktop/resources", |
| 483 | }, | 521 | }, |
| 484 | }; | 522 | }; |
| 485 | const publishConfig = resolveGitHubPublishConfig(); | 523 | const updateChannel = resolveDesktopUpdateChannel(version); |
| 524 | const publishConfig = resolveGitHubPublishConfig(updateChannel); | ||
| 486 | if (publishConfig) { | 525 | if (publishConfig) { |
| 487 | buildConfig.publish = [publishConfig]; | 526 | buildConfig.publish = [publishConfig]; |
| 488 | } else if (mockUpdates) { | 527 | } else if (mockUpdates) { |
| 489 | buildConfig.publish = [ | 528 | buildConfig.publish = [ |
| 490 | { | 529 | { |
| 491 | provider: "generic", | 530 | provider: "generic", |
| 492 | url: `http://localhost:${mockUpdateServerPort ?? 3000}`, | 531 | url: resolveMockUpdateServerUrl(mockUpdateServerPort), |
| 493 | }, | 532 | }, |
| 494 | ]; | 533 | ]; |
| 495 | } | 534 | } |
| @@ -533,20 +572,21 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( · 37 unmodified lines | |||
| 533 | const assertPlatformBuildResources = Effect.fn("assertPlatformBuildResources")(function* ( | 572 | const assertPlatformBuildResources = Effect.fn("assertPlatformBuildResources")(function* ( |
| 534 | platform: typeof BuildPlatform.Type, | 573 | platform: typeof BuildPlatform.Type, |
| 535 | stageResourcesDir: string, | 574 | stageResourcesDir: string, |
| 575 | iconAssets: DesktopBuildIconAssets, | ||
| 536 | verbose: boolean, | 576 | verbose: boolean, |
| 537 | ) { | 577 | ) { |
| 538 | if (platform === "mac") { | 578 | if (platform === "mac") { |
| 539 | yield* stageMacIcons(stageResourcesDir, verbose); | 579 | yield* stageMacIcons(stageResourcesDir, iconAssets.macIconPng, verbose); |
| 540 | return; | 580 | return; |
| 541 | } | 581 | } |
| 542 | 582 | ||
| 543 | if (platform === "linux") { | 583 | if (platform === "linux") { |
| 544 | yield* stageLinuxIcons(stageResourcesDir); | 584 | yield* stageLinuxIcons(stageResourcesDir, iconAssets.linuxIconPng); |
| 545 | return; | 585 | return; |
| 546 | } | 586 | } |
| 547 | 587 | ||
| 548 | if (platform === "win") { | 588 | if (platform === "win") { |
| 549 | yield* stageWindowsIcons(stageResourcesDir); | 589 | yield* stageWindowsIcons(stageResourcesDir, iconAssets.windowsIconIco); |
| 550 | } | 590 | } |
| 551 | }); | 591 | }); |
| 552 | 592 | ||
| @@ -614,6 +654,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( · 61 unmodified lines | |||
| 614 | }); | 654 | }); |
| 615 | 655 | ||
| 616 | const appVersion = options.version ?? serverPackageJson.version; | 656 | const appVersion = options.version ?? serverPackageJson.version; |
| 657 | const iconAssets = resolveDesktopBuildIconAssets(appVersion); | ||
| 617 | const commitHash = resolveGitCommitHash(repoRoot); | 658 | const commitHash = resolveGitCommitHash(repoRoot); |
| 618 | const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; | 659 | const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; |
| 619 | const stageRoot = yield* mkdir({ | 660 | const stageRoot = yield* mkdir({ |
| @@ -665,7 +706,16 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( · 45 unmodified lines | |||
| 665 | yield* fs.copy(distDirs.desktopResources, stageResourcesDir); | 706 | yield* fs.copy(distDirs.desktopResources, stageResourcesDir); |
| 666 | yield* fs.copy(distDirs.serverDist, path.join(stageAppDir, "apps/server/dist")); | 707 | yield* fs.copy(distDirs.serverDist, path.join(stageAppDir, "apps/server/dist")); |
| 667 | 708 | ||
| 668 | yield* assertPlatformBuildResources(options.platform, stageResourcesDir, options.verbose); | 709 | yield* assertPlatformBuildResources( |
| 710 | options.platform, | ||
| 711 | stageResourcesDir, | ||
| 712 | { | ||
| 713 | macIconPng: join(repoRoot, iconAssets.macIconPng), | ||
| 714 | linuxIconPng: join(repoRoot, iconAssets.linuxIconPng), | ||
| 715 | windowsIconIco: join(repoRoot, iconAssets.windowsIconIco), | ||
| 716 | }, | ||
| 717 | options.verbose, | ||
| 718 | ); | ||
| 669 | 719 | ||
| 670 | // electron-builder is filtering out stageResourcesDir directory in the AppImage for production | 720 | // electron-builder is filtering out stageResourcesDir directory in the AppImage for production |
| 671 | yield* fs.copy(stageResourcesDir, path.join(stageAppDir, "apps/desktop/prod-resources")); | 721 | yield* fs.copy(stageResourcesDir, path.join(stageAppDir, "apps/desktop/prod-resources")); |
| @@ -683,6 +733,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( · 11 unmodified lines | |||
| 683 | options.platform, | 733 | options.platform, |
| 684 | options.target, | 734 | options.target, |
| 685 | desktopPackageJson.productName ?? "T3 Code", | 735 | desktopPackageJson.productName ?? "T3 Code", |
| 736 | appVersion, | ||
| 686 | options.signed, | 737 | options.signed, |
| 687 | options.mockUpdates, | 738 | options.mockUpdates, |
| 688 | options.mockUpdateServerPort, | 739 | options.mockUpdateServerPort, |
| @@ -829,7 +880,8 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { · 140 unmodified lines | |||
| 829 | Flag.withDescription("Enable mock updates (env: T3CODE_DESKTOP_MOCK_UPDATES)."), | 880 | Flag.withDescription("Enable mock updates (env: T3CODE_DESKTOP_MOCK_UPDATES)."), |
| 830 | Flag.optional, | 881 | Flag.optional, |
| 831 | ), | 882 | ), |
| 832 | mockUpdateServerPort: Flag.string("mock-update-server-port").pipe( | 883 | mockUpdateServerPort: Flag.integer("mock-update-server-port").pipe( |
| 884 | Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), | ||
| 833 | Flag.withDescription("Mock update server port (env: T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT)."), | 885 | Flag.withDescription("Mock update server port (env: T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT)."), |
| 834 | Flag.optional, | 886 | Flag.optional, |
| 835 | ), | 887 | ), |
| @@ -6,6 +6,12 @@ export const BRAND_ASSET_PATHS = { · 5 unmodified lines | |||
| 6 | productionWebFavicon16Png: "assets/prod/t3-black-web-favicon-16x16.png", | 6 | productionWebFavicon16Png: "assets/prod/t3-black-web-favicon-16x16.png", |
| 7 | productionWebFavicon32Png: "assets/prod/t3-black-web-favicon-32x32.png", | 7 | productionWebFavicon32Png: "assets/prod/t3-black-web-favicon-32x32.png", |
| 8 | productionWebAppleTouchIconPng: "assets/prod/t3-black-web-apple-touch-180.png", | 8 | productionWebAppleTouchIconPng: "assets/prod/t3-black-web-apple-touch-180.png", |
| 9 | |||
| 10 | nightlyMacIconPng: "assets/nightly/blueprint-macos-1024.png", | ||
| 11 | nightlyLinuxIconPng: "assets/nightly/blueprint-universal-1024.png", | ||
| 12 | nightlyWindowsIconIco: "assets/nightly/blueprint-windows.ico", | ||
| 13 | |||
| 14 | developmentDesktopIconPng: "assets/dev/blueprint-macos-1024.png", | ||
| 9 | developmentWindowsIconIco: "assets/dev/blueprint-windows.ico", | 15 | developmentWindowsIconIco: "assets/dev/blueprint-windows.ico", |
| 10 | developmentWebFaviconIco: "assets/dev/blueprint-web-favicon.ico", | 16 | developmentWebFaviconIco: "assets/dev/blueprint-web-favicon.ico", |
| 11 | developmentWebFavicon16Png: "assets/dev/blueprint-web-favicon-16x16.png", | 17 | developmentWebFavicon16Png: "assets/dev/blueprint-web-favicon-16x16.png", |
| @@ -95,7 +95,7 @@ try { · 94 unmodified lines | |||
| 95 | }, | 95 | }, |
| 96 | ); | 96 | ); |
| 97 | 97 | ||
| 98 | execFileSync("bun", ["install", "--lockfile-only", "--ignore-scripts"], { | 98 | execFileSync("bun", ["install", "--ignore-scripts"], { |
| 99 | cwd: tempRoot, | 99 | cwd: tempRoot, |
| 100 | stdio: "inherit", | 100 | stdio: "inherit", |
| 101 | }); | 101 | }); |
| @@ -107,6 +107,40 @@ try { · 5 unmodified lines | |||
| 107 | "Expected bun.lock to contain the smoke version.", | 107 | "Expected bun.lock to contain the smoke version.", |
| 108 | ); | 108 | ); |
| 109 | 109 | ||
| 110 | const nightlyReleaseMetadata = execFileSync( | ||
| 111 | process.execPath, | ||
| 112 | [ | ||
| 113 | resolve(repoRoot, "scripts/resolve-nightly-release.ts"), | ||
| 114 | "--date", | ||
| 115 | "20260413", | ||
| 116 | "--run-number", | ||
| 117 | "321", | ||
| 118 | "--sha", | ||
| 119 | "abcdef1234567890", | ||
| 120 | "--root", | ||
| 121 | tempRoot, | ||
| 122 | ], | ||
| 123 | { | ||
| 124 | cwd: repoRoot, | ||
| 125 | encoding: "utf8", | ||
| 126 | }, | ||
| 127 | ); | ||
| 128 | assertContains( | ||
| 129 | nightlyReleaseMetadata, | ||
| 130 | "version=9.9.9-nightly.20260413.321", | ||
| 131 | "Expected nightly metadata to contain the derived nightly version.", | ||
| 132 | ); | ||
| 133 | assertContains( | ||
| 134 | nightlyReleaseMetadata, | ||
| 135 | "tag=nightly-v9.9.9-nightly.20260413.321", | ||
| 136 | "Expected nightly metadata to contain the derived nightly tag.", | ||
| 137 | ); | ||
| 138 | assertContains( | ||
| 139 | nightlyReleaseMetadata, | ||
| 140 | "name=T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)", | ||
| 141 | "Expected nightly metadata to include the short commit SHA in the release name.", | ||
| 142 | ); | ||
| 143 | |||
| 110 | const { arm64Path, x64Path } = writeMacManifestFixtures(tempRoot); | 144 | const { arm64Path, x64Path } = writeMacManifestFixtures(tempRoot); |
| 111 | execFileSync( | 145 | execFileSync( |
| 112 | process.execPath, | 146 | process.execPath, |
| @@ -0,0 +1,25 @@ | |||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | resolveNightlyBaseVersion, | ||
| 5 | resolveNightlyReleaseMetadata, | ||
| 6 | } from "./resolve-nightly-release.ts"; | ||
| 7 | |||
| 8 | it("strips prerelease and build metadata when deriving the nightly base version", () => { | ||
| 9 | assert.equal(resolveNightlyBaseVersion("0.0.17"), "0.0.17"); | ||
| 10 | assert.equal(resolveNightlyBaseVersion("9.9.9-smoke.0"), "9.9.9"); | ||
| 11 | assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3"); | ||
| 12 | }); | ||
| 13 | |||
| 14 | it("derives nightly metadata including the short commit sha in the release name", () => { | ||
| 15 | assert.deepStrictEqual( | ||
| 16 | resolveNightlyReleaseMetadata("9.9.9", "20260413", 321, "abcdef1234567890"), | ||
| 17 | { | ||
| 18 | baseVersion: "9.9.9", | ||
| 19 | version: "9.9.9-nightly.20260413.321", | ||
| 20 | tag: "nightly-v9.9.9-nightly.20260413.321", | ||
| 21 | name: "T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)", | ||
| 22 | shortSha: "abcdef123456", | ||
| 23 | }, | ||
| 24 | ); | ||
| 25 | }); | ||
| @@ -0,0 +1,126 @@ | |||
| 1 | #!/usr/bin/env node | ||
| 2 | |||
| 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | ||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 5 | import { Config, Effect, FileSystem, Option, Path, Schema } from "effect"; | ||
| 6 | import { Command, Flag } from "effect/unstable/cli"; | ||
| 7 | |||
| 8 | interface NightlyReleaseMetadata { | ||
| 9 | readonly baseVersion: string; | ||
| 10 | readonly version: string; | ||
| 11 | readonly tag: string; | ||
| 12 | readonly name: string; | ||
| 13 | readonly shortSha: string; | ||
| 14 | } | ||
| 15 | |||
| 16 | const DateSchema = Schema.String.check(Schema.isPattern(/^\d{8}$/)); | ||
| 17 | const RunNumberSchema = Schema.FiniteFromString.check( | ||
| 18 | Schema.isInt(), | ||
| 19 | Schema.isGreaterThanOrEqualTo(1), | ||
| 20 | ); | ||
| 21 | const ShaSchema = Schema.String.check(Schema.isPattern(/^[0-9a-f]{7,40}$/i)); | ||
| 22 | const DesktopPackageJsonSchema = Schema.Struct({ | ||
| 23 | version: Schema.NonEmptyString, | ||
| 24 | }); | ||
| 25 | |||
| 26 | const RepoRoot = Effect.service(Path.Path).pipe( | ||
| 27 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), | ||
| 28 | ); | ||
| 29 | const decodeDesktopPackageJson = Schema.decodeUnknownEffect( | ||
| 30 | Schema.fromJsonString(DesktopPackageJsonSchema), | ||
| 31 | ); | ||
| 32 | |||
| 33 | export const resolveNightlyBaseVersion = (version: string) => version.replace(/[-+].*$/, ""); | ||
| 34 | |||
| 35 | export const resolveNightlyReleaseMetadata = ( | ||
| 36 | baseVersion: string, | ||
| 37 | date: string, | ||
| 38 | runNumber: number, | ||
| 39 | sha: string, | ||
| 40 | ) => { | ||
| 41 | const shortSha = sha.slice(0, 12); | ||
| 42 | const version = `${baseVersion}-nightly.${date}.${runNumber}`; | ||
| 43 | return { | ||
| 44 | baseVersion, | ||
| 45 | version, | ||
| 46 | tag: `nightly-v${version}`, | ||
| 47 | name: `T3 Code Nightly ${version} (${shortSha})`, | ||
| 48 | shortSha, | ||
| 49 | }; | ||
| 50 | }; | ||
| 51 | |||
| 52 | const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( | ||
| 53 | rootDir: string | undefined, | ||
| 54 | ) { | ||
| 55 | const fs = yield* FileSystem.FileSystem; | ||
| 56 | const path = yield* Path.Path; | ||
| 57 | const workspaceRoot = rootDir ? path.resolve(rootDir) : yield* RepoRoot; | ||
| 58 | const packageJsonPath = path.join(workspaceRoot, "apps/desktop/package.json"); | ||
| 59 | const packageJson = yield* fs | ||
| 60 | .readFileString(packageJsonPath) | ||
| 61 | .pipe(Effect.flatMap(decodeDesktopPackageJson)); | ||
| 62 | return resolveNightlyBaseVersion(packageJson.version); | ||
| 63 | }); | ||
| 64 | |||
| 65 | const writeOutput = Effect.fn("writeOutput")(function* ( | ||
| 66 | metadata: NightlyReleaseMetadata, | ||
| 67 | writeGithubOutput: boolean, | ||
| 68 | ) { | ||
| 69 | const fs = yield* FileSystem.FileSystem; | ||
| 70 | |||
| 71 | const entries = [ | ||
| 72 | ["base_version", metadata.baseVersion], | ||
| 73 | ["version", metadata.version], | ||
| 74 | ["tag", metadata.tag], | ||
| 75 | ["name", metadata.name], | ||
| 76 | ["short_sha", metadata.shortSha], | ||
| 77 | ] as const; | ||
| 78 | |||
| 79 | if (writeGithubOutput) { | ||
| 80 | const githubOutputPath = yield* Config.nonEmptyString("GITHUB_OUTPUT"); | ||
| 81 | const serialized = entries.map(([key, value]) => `${key}=${value}\n`).join(""); | ||
| 82 | yield* fs.writeFileString(githubOutputPath, serialized, { flag: "a" }); | ||
| 83 | } else { | ||
| 84 | for (const [key, value] of entries) { | ||
| 85 | console.log(`${key}=${value}`); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | }); | ||
| 89 | |||
| 90 | const command = Command.make( | ||
| 91 | "resolve-nightly-release", | ||
| 92 | { | ||
| 93 | date: Flag.string("date").pipe( | ||
| 94 | Flag.withSchema(DateSchema), | ||
| 95 | Flag.withDescription("Nightly build date in YYYYMMDD."), | ||
| 96 | ), | ||
| 97 | runNumber: Flag.string("run-number").pipe( | ||
| 98 | Flag.withSchema(RunNumberSchema), | ||
| 99 | Flag.withDescription("GitHub Actions run number."), | ||
| 100 | ), | ||
| 101 | sha: Flag.string("sha").pipe( | ||
| 102 | Flag.withSchema(ShaSchema), | ||
| 103 | Flag.withDescription("Commit sha for the nightly build."), | ||
| 104 | ), | ||
| 105 | githubOutput: Flag.boolean("github-output").pipe( | ||
| 106 | Flag.withDescription("Write values to GITHUB_OUTPUT instead of stdout."), | ||
| 107 | Flag.withDefault(false), | ||
| 108 | ), | ||
| 109 | root: Flag.string("root").pipe( | ||
| 110 | Flag.withDescription("Workspace root used to resolve apps/desktop/package.json."), | ||
| 111 | Flag.optional, | ||
| 112 | ), | ||
| 113 | }, | ||
| 114 | ({ date, runNumber, sha, githubOutput, root }) => | ||
| 115 | readDesktopBaseVersion(Option.getOrUndefined(root)).pipe( | ||
| 116 | Effect.map((baseVersion) => resolveNightlyReleaseMetadata(baseVersion, date, runNumber, sha)), | ||
| 117 | Effect.flatMap((metadata) => writeOutput(metadata, githubOutput)), | ||
| 118 | ), | ||
| 119 | ).pipe(Command.withDescription("Resolve nightly release version metadata.")); | ||
| 120 | |||
| 121 | if (import.meta.main) { | ||
| 122 | Command.run(command, { version: "0.0.0" }).pipe( | ||
| 123 | Effect.provide(NodeServices.layer), | ||
| 124 | NodeRuntime.runMain, | ||
| 125 | ); | ||
| 126 | } | ||
| @@ -0,0 +1,203 @@ | |||
| 1 | #!/usr/bin/env node | ||
| 2 | |||
| 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | ||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 5 | import { Array, Config, Effect, FileSystem, Schema, Stream, String } from "effect"; | ||
| 6 | import { Command, Flag } from "effect/unstable/cli"; | ||
| 7 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | ||
| 8 | |||
| 9 | const ReleaseChannel = Schema.Literals(["stable", "nightly"]); | ||
| 10 | type ReleaseChannel = typeof ReleaseChannel.Type; | ||
| 11 | |||
| 12 | interface StableVersion { | ||
| 13 | readonly major: number; | ||
| 14 | readonly minor: number; | ||
| 15 | readonly patch: number; | ||
| 16 | readonly prerelease: ReadonlyArray<string>; | ||
| 17 | } | ||
| 18 | |||
| 19 | interface NightlyVersion { | ||
| 20 | readonly major: number; | ||
| 21 | readonly minor: number; | ||
| 22 | readonly patch: number; | ||
| 23 | readonly date: number; | ||
| 24 | readonly runNumber: number; | ||
| 25 | } | ||
| 26 | |||
| 27 | const parseNumericIdentifier = (identifier: string): number | undefined => | ||
| 28 | /^\d+$/.test(identifier) ? Number(identifier) : undefined; | ||
| 29 | |||
| 30 | const comparePrereleaseIdentifiers = (left: string, right: string): number => { | ||
| 31 | const leftNumeric = parseNumericIdentifier(left); | ||
| 32 | const rightNumeric = parseNumericIdentifier(right); | ||
| 33 | |||
| 34 | if (leftNumeric !== undefined && rightNumeric !== undefined) { | ||
| 35 | return leftNumeric - rightNumeric; | ||
| 36 | } | ||
| 37 | if (leftNumeric !== undefined) { | ||
| 38 | return -1; | ||
| 39 | } | ||
| 40 | if (rightNumeric !== undefined) { | ||
| 41 | return 1; | ||
| 42 | } | ||
| 43 | return left.localeCompare(right); | ||
| 44 | }; | ||
| 45 | |||
| 46 | const compareStableVersions = (left: StableVersion, right: StableVersion): number => { | ||
| 47 | if (left.major !== right.major) return left.major - right.major; | ||
| 48 | if (left.minor !== right.minor) return left.minor - right.minor; | ||
| 49 | if (left.patch !== right.patch) return left.patch - right.patch; | ||
| 50 | |||
| 51 | const leftHasPrerelease = left.prerelease.length > 0; | ||
| 52 | const rightHasPrerelease = right.prerelease.length > 0; | ||
| 53 | if (!leftHasPrerelease && !rightHasPrerelease) return 0; | ||
| 54 | if (!leftHasPrerelease) return 1; | ||
| 55 | if (!rightHasPrerelease) return -1; | ||
| 56 | |||
| 57 | const maxLength = Math.max(left.prerelease.length, right.prerelease.length); | ||
| 58 | for (let index = 0; index < maxLength; index += 1) { | ||
| 59 | const leftIdentifier = left.prerelease[index]; | ||
| 60 | const rightIdentifier = right.prerelease[index]; | ||
| 61 | if (leftIdentifier === undefined) return -1; | ||
| 62 | if (rightIdentifier === undefined) return 1; | ||
| 63 | |||
| 64 | const comparison = comparePrereleaseIdentifiers(leftIdentifier, rightIdentifier); | ||
| 65 | if (comparison !== 0) return comparison; | ||
| 66 | } | ||
| 67 | |||
| 68 | return 0; | ||
| 69 | }; | ||
| 70 | |||
| 71 | const parseStableTag = (tag: string): StableVersion | undefined => { | ||
| 72 | const match = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(tag); | ||
| 73 | if (!match) return undefined; | ||
| 74 | |||
| 75 | const [, major, minor, patch, prerelease] = match; | ||
| 76 | if (!major || !minor || !patch) return undefined; | ||
| 77 | |||
| 78 | return { | ||
| 79 | major: Number(major), | ||
| 80 | minor: Number(minor), | ||
| 81 | patch: Number(patch), | ||
| 82 | prerelease: prerelease ? prerelease.split(".") : [], | ||
| 83 | }; | ||
| 84 | }; | ||
| 85 | |||
| 86 | const compareNightlyVersions = (left: NightlyVersion, right: NightlyVersion): number => { | ||
| 87 | if (left.major !== right.major) return left.major - right.major; | ||
| 88 | if (left.minor !== right.minor) return left.minor - right.minor; | ||
| 89 | if (left.patch !== right.patch) return left.patch - right.patch; | ||
| 90 | if (left.date !== right.date) return left.date - right.date; | ||
| 91 | return left.runNumber - right.runNumber; | ||
| 92 | }; | ||
| 93 | |||
| 94 | const parseNightlyTag = (tag: string): NightlyVersion | undefined => { | ||
| 95 | const match = /^nightly-v(\d+)\.(\d+)\.(\d+)-nightly\.(\d{8})\.(\d+)$/.exec(tag); | ||
| 96 | if (!match) return undefined; | ||
| 97 | |||
| 98 | const [, major, minor, patch, date, runNumber] = match; | ||
| 99 | if (!major || !minor || !patch || !date || !runNumber) return undefined; | ||
| 100 | |||
| 101 | return { | ||
| 102 | major: Number(major), | ||
| 103 | minor: Number(minor), | ||
| 104 | patch: Number(patch), | ||
| 105 | date: Number(date), | ||
| 106 | runNumber: Number(runNumber), | ||
| 107 | }; | ||
| 108 | }; | ||
| 109 | |||
| 110 | const resolvePreviousReleaseTag = ( | ||
| 111 | channel: ReleaseChannel, | ||
| 112 | currentTag: string, | ||
| 113 | tags: ReadonlyArray<string>, | ||
| 114 | ): string | undefined => { | ||
| 115 | if (channel === "stable") { | ||
| 116 | const current = parseStableTag(currentTag); | ||
| 117 | if (!current) { | ||
| 118 | throw new Error(`Invalid stable release tag '${currentTag}'.`); | ||
| 119 | } | ||
| 120 | |||
| 121 | const candidates = tags | ||
| 122 | .map((tag) => ({ tag, parsed: parseStableTag(tag) })) | ||
| 123 | .filter( | ||
| 124 | (entry): entry is { tag: string; parsed: StableVersion } => entry.parsed !== undefined, | ||
| 125 | ) | ||
| 126 | .filter((entry) => compareStableVersions(entry.parsed, current) < 0) | ||
| 127 | .toSorted((left, right) => compareStableVersions(right.parsed, left.parsed)); | ||
| 128 | |||
| 129 | return candidates[0]?.tag; | ||
| 130 | } | ||
| 131 | |||
| 132 | const current = parseNightlyTag(currentTag); | ||
| 133 | if (!current) { | ||
| 134 | throw new Error(`Invalid nightly release tag '${currentTag}'.`); | ||
| 135 | } | ||
| 136 | |||
| 137 | const candidates = tags | ||
| 138 | .map((tag) => ({ tag, parsed: parseNightlyTag(tag) })) | ||
| 139 | .filter((entry): entry is { tag: string; parsed: NightlyVersion } => entry.parsed !== undefined) | ||
| 140 | .filter((entry) => compareNightlyVersions(entry.parsed, current) < 0) | ||
| 141 | .toSorted((left, right) => compareNightlyVersions(right.parsed, left.parsed)); | ||
| 142 | |||
| 143 | return candidates[0]?.tag; | ||
| 144 | }; | ||
| 145 | |||
| 146 | const listGitTags = Effect.fn("listGitTags")(function* () { | ||
| 147 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | ||
| 148 | const child = yield* spawner.spawn(ChildProcess.make("git", ["tag", "--list"])); | ||
| 149 | const tags = yield* child.stdout.pipe( | ||
| 150 | Stream.decodeText(), | ||
| 151 | Stream.runFold( | ||
| 152 | () => "", | ||
| 153 | (acc, chunk) => acc + chunk, | ||
| 154 | ), | ||
| 155 | Effect.map(String.split(/\r?\n/)), | ||
| 156 | Effect.map(Array.map(String.trim)), | ||
| 157 | Effect.map(Array.filter(String.isNonEmpty)), | ||
| 158 | ); | ||
| 159 | return tags; | ||
| 160 | }); | ||
| 161 | |||
| 162 | const writeOutput = Effect.fn("writeOutput")(function* ( | ||
| 163 | previousTag: string | undefined, | ||
| 164 | writeGithubOutput: boolean, | ||
| 165 | ) { | ||
| 166 | const entry = `previous_tag=${previousTag ?? ""}\n`; | ||
| 167 | |||
| 168 | if (writeGithubOutput) { | ||
| 169 | const fs = yield* FileSystem.FileSystem; | ||
| 170 | const githubOutputPath = yield* Config.nonEmptyString("GITHUB_OUTPUT"); | ||
| 171 | yield* fs.writeFileString(githubOutputPath, entry, { flag: "a" }); | ||
| 172 | return; | ||
| 173 | } | ||
| 174 | |||
| 175 | process.stdout.write(entry); | ||
| 176 | }); | ||
| 177 | |||
| 178 | const command = Command.make( | ||
| 179 | "resolve-previous-release-tag", | ||
| 180 | { | ||
| 181 | channel: Flag.choice("channel", ReleaseChannel.literals).pipe( | ||
| 182 | Flag.withDescription("Release channel whose previous tag should be resolved."), | ||
| 183 | ), | ||
| 184 | currentTag: Flag.string("current-tag").pipe( | ||
| 185 | Flag.withDescription("Current release tag to compare against."), | ||
| 186 | ), | ||
| 187 | githubOutput: Flag.boolean("github-output").pipe( | ||
| 188 | Flag.withDescription("Write values to GITHUB_OUTPUT instead of stdout."), | ||
| 189 | Flag.withDefault(false), | ||
| 190 | ), | ||
| 191 | }, | ||
| 192 | ({ channel, currentTag, githubOutput }) => | ||
| 193 | listGitTags().pipe( | ||
| 194 | Effect.map((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), | ||
| 195 | Effect.flatMap((previousTag) => writeOutput(previousTag, githubOutput)), | ||
| 196 | ), | ||
| 197 | ).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series.")); | ||
| 198 | |||
| 199 | Command.run(command, { version: "0.0.0" }).pipe( | ||
| 200 | Effect.scoped, | ||
| 201 | Effect.provide(NodeServices.layer), | ||
| 202 | NodeRuntime.runMain, | ||
| 203 | ); | ||