Repository navigation
fix(release): wait for npm to serve platform packages before publishing t3 #18251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e94c843
aabd910
0dc4df1
bfe4e79
6ac6887
c80603c
63b5e62
b4f6d5e
c809380
14d7ed0
d16891f
27a9505
8450db3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,9 @@ import * as FileSystem from "effect/FileSystem"; | |||||||||||||||||||||||||||||||||||
| import * as Logger from "effect/Logger"; | ||||||||||||||||||||||||||||||||||||
| import * as Option from "effect/Option"; | ||||||||||||||||||||||||||||||||||||
| import * as Path from "effect/Path"; | ||||||||||||||||||||||||||||||||||||
| import * as Stream from "effect/Stream"; | ||||||||||||||||||||||||||||||||||||
| import { Command, Flag } from "effect/cli"; | ||||||||||||||||||||||||||||||||||||
| import { FetchHttpClient } from "effect/http"; | ||||||||||||||||||||||||||||||||||||
| import { ChildProcess, ChildProcessSpawner } from "effect/process"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts"; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -19,7 +21,12 @@ import { | |||||||||||||||||||||||||||||||||||
| ServerCliDevelopmentIconTargetMissingError, | ||||||||||||||||||||||||||||||||||||
| ServerCliExecutableImportError, | ||||||||||||||||||||||||||||||||||||
| } from "./cliErrors.ts"; | ||||||||||||||||||||||||||||||||||||
| import { publishPlatformsThenLauncher } from "./publishOrder.ts"; | ||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||
| checkNpmVersionInstallable, | ||||||||||||||||||||||||||||||||||||
| npmPublishRegistry, | ||||||||||||||||||||||||||||||||||||
| publishPlatformsThenLauncher, | ||||||||||||||||||||||||||||||||||||
| readLauncherPlatformPackages, | ||||||||||||||||||||||||||||||||||||
| } from "./publishOrder.ts"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const RepoRoot = Effect.service(Path.Path).pipe( | ||||||||||||||||||||||||||||||||||||
| Effect.flatMap((path) => path.fromFileUrl(new URL("../../..", import.meta.url))), | ||||||||||||||||||||||||||||||||||||
|
|
@@ -40,6 +47,40 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Stan | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // What `npm config get <keys>` prints in `cwd`, where `npm publish` runs, so | ||||||||||||||||||||||||||||||||||||
| // it reads the same .npmrc files and npm_config_* variables. npm exits non-zero | ||||||||||||||||||||||||||||||||||||
| // rather than print a protected value (a URL password, a UUID or an `npm_` | ||||||||||||||||||||||||||||||||||||
| // token), which stops the publish before any upload; configure such a registry | ||||||||||||||||||||||||||||||||||||
| // without them. | ||||||||||||||||||||||||||||||||||||
| const npmConfigGet = Effect.fn("npmConfigGet")(function* ( | ||||||||||||||||||||||||||||||||||||
| cwd: string, | ||||||||||||||||||||||||||||||||||||
| keys: ReadonlyArray<string>, | ||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | ||||||||||||||||||||||||||||||||||||
| const spawnCommand = yield* resolveSpawnCommand("npm", ["config", "get", ...keys]); | ||||||||||||||||||||||||||||||||||||
| const child = yield* spawner.spawn( | ||||||||||||||||||||||||||||||||||||
| ChildProcess.make(spawnCommand.command, spawnCommand.args, { | ||||||||||||||||||||||||||||||||||||
| cwd, | ||||||||||||||||||||||||||||||||||||
| stdin: "ignore", | ||||||||||||||||||||||||||||||||||||
| stderr: "inherit", | ||||||||||||||||||||||||||||||||||||
| shell: spawnCommand.shell, | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| const [printed, exitCode] = yield* Effect.all( | ||||||||||||||||||||||||||||||||||||
| [child.stdout.pipe(Stream.decodeText(), Stream.mkString), child.exitCode], | ||||||||||||||||||||||||||||||||||||
| { concurrency: "unbounded" }, | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| if (exitCode !== 0) { | ||||||||||||||||||||||||||||||||||||
| return yield* new ServerCliCommandExitError({ | ||||||||||||||||||||||||||||||||||||
| command: spawnCommand.command, | ||||||||||||||||||||||||||||||||||||
| args: spawnCommand.args, | ||||||||||||||||||||||||||||||||||||
| cwd, | ||||||||||||||||||||||||||||||||||||
| exitCode, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return printed; | ||||||||||||||||||||||||||||||||||||
| }, Effect.scoped); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* ( | ||||||||||||||||||||||||||||||||||||
| repoRoot: string, | ||||||||||||||||||||||||||||||||||||
| serverDir: string, | ||||||||||||||||||||||||||||||||||||
|
|
@@ -170,6 +211,9 @@ const buildExeCmd = Command.make( | |||||||||||||||||||||||||||||||||||
| * Publishes the tarballs scripts/build-npm-platform-packages.ts produced: | ||||||||||||||||||||||||||||||||||||
| * every `@t3code/t3-<platform>.tgz` first, `t3.tgz` (the launcher) last, so | ||||||||||||||||||||||||||||||||||||
| * the launcher is never installable before the executables it depends on. | ||||||||||||||||||||||||||||||||||||
| * Before the launcher, it waits up to `READY_DEADLINE` (publishOrder.ts) for | ||||||||||||||||||||||||||||||||||||
| * npm to serve each platform version, since an upload can 404 for minutes | ||||||||||||||||||||||||||||||||||||
| * after it succeeds. | ||||||||||||||||||||||||||||||||||||
| * Tarballs rather than directories because `npm publish <dir>` strips the | ||||||||||||||||||||||||||||||||||||
| * `node_modules/` the executable loads its native addons from. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
|
|
@@ -208,6 +252,21 @@ const publishCmd = Command.make( | |||||||||||||||||||||||||||||||||||
| if (!(yield* fs.exists(launcherTarball))) { | ||||||||||||||||||||||||||||||||||||
| return yield* new ServerCliBuildAssetMissingError({ assetPath: launcherTarball }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| const platformPackages = yield* readLauncherPlatformPackages( | ||||||||||||||||||||||||||||||||||||
| path.join(packagesDir, "t3", "package.json"), | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| for (const { name } of platformPackages) { | ||||||||||||||||||||||||||||||||||||
| const tarball = path.join(packagesDir, `${name}.tgz`); | ||||||||||||||||||||||||||||||||||||
| if (!platformTarballs.includes(tarball)) { | ||||||||||||||||||||||||||||||||||||
| return yield* new ServerCliBuildAssetMissingError({ assetPath: tarball }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+258
to
+263
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject platform tarballs that the launcher manifest does not list. This loop only checks that each manifest package has a tarball. It does not check the other direction. Suppose Proposed fix--- "a/apps/server/scripts/cli.ts"
+++ "b/apps/server/scripts/cli.ts"
@@ -255,12 +255,17 @@
const platformPackages = yield* readLauncherPlatformPackages(
path.join(packagesDir, "t3", "package.json"),
);
for (const { name } of platformPackages) {
const tarball = path.join(packagesDir, `${name}.tgz`);
if (!platformTarballs.includes(tarball)) {
return yield* new ServerCliBuildAssetMissingError({ assetPath: tarball });
}
}
+ const expected = new Set(platformPackages.map(({ name }) => path.join(packagesDir, `${name}.tgz`)));
+ const unexpected = platformTarballs.find((tarball) => !expected.has(tarball));
+ if (unexpected !== undefined) {
+ return yield* new ServerCliLauncherManifestInvalidError({ assetPath: path.join(packagesDir, "t3", "package.json"), cause: new Error("unlisted platform tarball") });
+ }
// A real run checks for the platform packages where npm publishes them,
// read before any upload. A dry run checks nothing.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // A real run checks for the platform packages where npm publishes them, | ||||||||||||||||||||||||||||||||||||
| // read before any upload. A dry run checks nothing. | ||||||||||||||||||||||||||||||||||||
| const registry = config.dryRun | ||||||||||||||||||||||||||||||||||||
| ? "" | ||||||||||||||||||||||||||||||||||||
| : yield* npmPublishRegistry((keys) => npmConfigGet(packagesDir, keys)); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const args = ["publish", "--access", config.access, "--tag", config.tag]; | ||||||||||||||||||||||||||||||||||||
| if (config.provenance) args.push("--provenance"); | ||||||||||||||||||||||||||||||||||||
|
|
@@ -227,7 +286,14 @@ const publishCmd = Command.make( | |||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Each publish takes about 17s, so the platform packages go at once. | ||||||||||||||||||||||||||||||||||||
| yield* publishPlatformsThenLauncher({ platformTarballs, launcherTarball, publish }); | ||||||||||||||||||||||||||||||||||||
| yield* publishPlatformsThenLauncher({ | ||||||||||||||||||||||||||||||||||||
| platformTarballs, | ||||||||||||||||||||||||||||||||||||
| launcherTarball, | ||||||||||||||||||||||||||||||||||||
| publish, | ||||||||||||||||||||||||||||||||||||
| dryRun: config.dryRun, | ||||||||||||||||||||||||||||||||||||
| platformPackages, | ||||||||||||||||||||||||||||||||||||
| checkInstallable: (pkg) => checkNpmVersionInstallable(registry, pkg), | ||||||||||||||||||||||||||||||||||||
| }).pipe(Effect.provide(FetchHttpClient.layer)); | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| ).pipe( | ||||||||||||||||||||||||||||||||||||
| Command.withDescription( | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: pingdotgg/t3code
Length of output: 9851
🏁 Script executed:
Repository: pingdotgg/t3code
Length of output: 10164
Increase the
publish_clijob timeout to cover the full readiness deadline.READY_DEADLINEis 25 minutes, whiletimeout-minutes: 35applies to the entire job. If preparation and platform publishing take more than 10 minutes, GitHub can cancel the job during readiness polling. The platform packages can then be published whilet3remains unpublished.Set the job timeout to at least the 25-minute readiness deadline plus the worst-case time for the preceding and following steps.
🤖 Prompt for AI Agents