chore: script release tag push and trim daemon log noise

This commit is contained in:
Mohamed Boudra
2026-02-12 19:05:51 +07:00
parent 6f6677fc7a
commit 734cf7af20
8 changed files with 100 additions and 132 deletions
+8 -7
View File
@@ -150,21 +150,22 @@ Run `npx expo-doctor` to diagnose version mismatches and native module issues.
## Release playbook
Use the scripted release flow from repo root. Avoid manual version bumps or publish commands unless debugging.
Use the scripted release flow from repo root. Avoid manual version bumps, manual tags, or ad hoc publish commands unless debugging.
```bash
# 1) bump all workspaces and refresh workspace links
npm run version:all:patch
# Recommended: full patch release (bump, check, publish, push branch+tag)
npm run release:patch
# 2) run release gate checks (typecheck, build, pack dry-run)
# Manual, step-by-step fallback:
npm run version:all:patch # npm version across all workspaces (creates commit + local tag)
npm run release:check
# 3) publish relay/server/cli packages
npm run release:publish
npm run release:push # pushes HEAD and current version tag (triggers desktop release)
```
Notes:
- `release:prepare` is part of the flow and refreshes workspace `node_modules` links to prevent stale local package types during release checks.
- `version:all:*` uses `npm version` with workspace support and runs the root `version` lifecycle script to sync internal `@getpaseo/*` dependency versions before the release commit/tag is created.
- `release:prepare` refreshes workspace `node_modules` links to prevent stale local package types during release checks.
- If `release:publish` fails after a successful publish of one workspace, re-run `npm run release:publish`; npm will skip already-published versions and continue where possible.
## Orchestrator Mode
+7 -3
View File
@@ -76,11 +76,15 @@ See [paseo.sh/docs](https://paseo.sh/docs) for full documentation.
Desktop app binaries are built and attached to a GitHub Release when you push a version tag (for example `v0.1.0` or `desktop-v0.1.0`).
```bash
git tag v0.1.0
git push origin v0.1.0
npm run version:all:patch
npm run release:push
```
If you prefer, `npm version` can be used to create and push a version tag.
For the full package release flow, use:
```bash
npm run release:patch
```
This triggers the `Desktop Release` workflow (`.github/workflows/desktop-release.yml`).
+9 -4
View File
@@ -32,14 +32,19 @@
"dev:desktop": "npm run dev --workspace=@getpaseo/desktop",
"build:desktop": "npm run build --workspace=@getpaseo/desktop",
"cli": "npx tsx packages/cli/src/index.js",
"version": "npm run version:sync-internal && npm run release:prepare && git add -A",
"version:sync-internal": "node scripts/sync-workspace-versions.mjs",
"release:prepare": "npm install --workspaces --include-workspace-root",
"version:all:patch": "npm version patch --workspaces --include-workspace-root --no-git-tag-version && npm run version:sync-internal && npm run release:prepare",
"version:all:minor": "npm version minor --workspaces --include-workspace-root --no-git-tag-version && npm run version:sync-internal && npm run release:prepare",
"version:all:major": "npm version major --workspaces --include-workspace-root --no-git-tag-version && npm run version:sync-internal && npm run release:prepare",
"version:all:patch": "npm version patch --workspaces --include-workspace-root --message \"chore(release): cut %s\"",
"version:all:minor": "npm version minor --workspaces --include-workspace-root --message \"chore(release): cut %s\"",
"version:all:major": "npm version major --workspaces --include-workspace-root --message \"chore(release): cut %s\"",
"release:check": "npm run release:prepare && npm run typecheck --workspace=@getpaseo/relay && npm run typecheck --workspace=@getpaseo/server && npm run typecheck --workspace=@getpaseo/cli && npm run build --workspace=@getpaseo/relay && npm run build --workspace=@getpaseo/server && npm run build --workspace=@getpaseo/cli && npm pack --dry-run --workspace=@getpaseo/relay && npm pack --dry-run --workspace=@getpaseo/server && npm pack --dry-run --workspace=@getpaseo/cli",
"release:publish:dry-run": "npm publish --dry-run --workspace=@getpaseo/relay --access public && npm publish --dry-run --workspace=@getpaseo/server --access public && npm publish --dry-run --workspace=@getpaseo/cli --access public",
"release:publish": "npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public"
"release:publish": "npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public",
"release:push": "node scripts/push-current-release-tag.mjs",
"release:patch": "npm run version:all:patch && npm run release:check && npm run release:publish && npm run release:push",
"release:minor": "npm run version:all:minor && npm run release:check && npm run release:publish && npm run release:push",
"release:major": "npm run version:all:major && npm run release:check && npm run release:publish && npm run release:push"
},
"devDependencies": {
"concurrently": "^9.2.1",
-17
View File
@@ -52,7 +52,6 @@ import { createAgentMcpServer } from "./agent/mcp-server.js";
import { createAllClients, shutdownProviders } from "./agent/provider-registry.js";
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
import {
buildOfferEndpoints,
createConnectionOfferV2,
encodeOfferToFragmentUrl,
} from "./connection-offer.js";
@@ -524,20 +523,6 @@ export async function createPaseoDaemon(
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
const directEndpoints = buildOfferEndpoints({
listenHost: listenTarget.host,
port: listenTarget.port,
});
logger.info(
{
serverId,
endpoints: directEndpoints,
wsUrls: directEndpoints.map((endpoint) => `ws://${endpoint}/ws`),
},
"direct_connect"
);
if (relayEnabled) {
const offer = await createConnectionOfferV2({
serverId,
@@ -547,8 +532,6 @@ export async function createPaseoDaemon(
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
logger.info({ url }, "pairing_offer");
} else {
logger.info("relay_disabled");
}
if (relayEnabled) {
@@ -197,10 +197,10 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
});
try {
const directConnect = await new Promise<{ endpoints: string[] }>((resolve, reject) => {
const sawListeningLog = await new Promise<boolean>((resolve, reject) => {
const timeout = setTimeout(() => {
proc.kill();
reject(new Error("timed out waiting for direct_connect log"));
reject(new Error("timed out waiting for server listening log"));
}, 15000);
const onData = (data: Buffer) => {
@@ -213,16 +213,12 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
reject(new Error("unexpected pairing_offer log when --no-relay is set"));
return;
}
if (!line.includes("direct_connect")) continue;
try {
const parsed = JSON.parse(line) as { msg?: string; endpoints?: unknown };
if (parsed.msg !== "direct_connect") continue;
const endpoints = Array.isArray(parsed.endpoints)
? parsed.endpoints.filter((v) => typeof v === "string")
: [];
if (endpoints.length === 0) continue;
const parsed = JSON.parse(line) as { msg?: string };
if (parsed.msg !== `Server listening on http://0.0.0.0:${port}`) continue;
clearTimeout(timeout);
resolve({ endpoints });
resolve(true);
return;
} catch {
// ignore
@@ -243,8 +239,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
});
});
expect(directConnect.endpoints).toContain(`localhost:${port}`);
expect(directConnect.endpoints).toContain(`192.168.1.12:${port}`);
expect(sawListeningLog).toBe(true);
} catch (err) {
throw new Error(
`failed; stdout so far:\\n${stdoutLines.join("")}\\n\\n${String(err)}`
@@ -54,15 +54,15 @@ describe("sherpa model downloader", () => {
).rejects.toThrow(/auto-download/i);
});
test("ensureSherpaOnnxModel logs artifact download progress", async () => {
test("ensureSherpaOnnxModel logs lifecycle events without progress spam", async () => {
const modelsDir = makeTmpDir();
const progressLogs: Array<Record<string, unknown>> = [];
const infoMessages: string[] = [];
const loggerWithSpy = {
child: () => loggerWithSpy,
info: (obj?: unknown, msg?: string) => {
if (msg === "Downloading model artifact" && obj && typeof obj === "object") {
progressLogs.push(obj as Record<string, unknown>);
info: (_obj?: unknown, msg?: string) => {
if (typeof msg === "string") {
infoMessages.push(msg);
}
},
error: () => undefined,
@@ -90,9 +90,8 @@ describe("sherpa model downloader", () => {
}
expect(fetchMock).toHaveBeenCalled();
expect(progressLogs.length).toBeGreaterThan(0);
const final = progressLogs.at(-1);
expect(final?.modelId).toBe("pocket-tts-onnx-int8");
expect(final?.pct).toBe(100);
expect(infoMessages).toContain("Starting model download");
expect(infoMessages).toContain("Model download completed");
expect(infoMessages).not.toContain("Downloading model artifact");
});
});
@@ -1,7 +1,7 @@
import { createWriteStream } from "node:fs";
import { mkdir, rename, rm, stat } from "node:fs/promises";
import path from "node:path";
import { Readable, Transform } from "node:stream";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { spawn } from "node:child_process";
import type pino from "pino";
@@ -39,31 +39,13 @@ async function hasRequiredFiles(modelDir: string, requiredFiles: string[]): Prom
return true;
}
const UNKNOWN_SIZE_PROGRESS_BYTES_STEP = 5 * 1024 * 1024;
const UNKNOWN_SIZE_PROGRESS_MS_STEP = 1000;
type DownloadToFileOptions = {
url: string;
outputPath: string;
logger: pino.Logger;
modelId: SherpaOnnxModelId;
artifact: string;
};
function parseContentLength(res: Response): number | null {
const raw = res.headers.get("content-length");
if (!raw) {
return null;
}
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return parsed;
}
async function downloadToFile(options: DownloadToFileOptions): Promise<void> {
const { url, outputPath, logger, modelId, artifact } = options;
const { url, outputPath } = options;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
@@ -76,62 +58,9 @@ async function downloadToFile(options: DownloadToFileOptions): Promise<void> {
await mkdir(path.dirname(outputPath), { recursive: true });
const nodeStream = Readable.fromWeb(res.body as any);
const totalBytes = parseContentLength(res);
let downloadedBytes = 0;
let lastLoggedPct = -1;
let lastLoggedBytes = 0;
let lastLoggedAt = 0;
const logProgress = (force: boolean): void => {
const pct =
totalBytes && totalBytes > 0
? Math.min(100, Math.floor((downloadedBytes * 100) / totalBytes))
: null;
if (!force) {
if (pct !== null) {
if (pct <= lastLoggedPct) {
return;
}
} else {
const now = Date.now();
const advancedBytes = downloadedBytes - lastLoggedBytes;
if (advancedBytes < UNKNOWN_SIZE_PROGRESS_BYTES_STEP && now - lastLoggedAt < UNKNOWN_SIZE_PROGRESS_MS_STEP) {
return;
}
}
}
logger.info(
{
modelId,
artifact,
url,
downloadedBytes,
totalBytes,
pct,
},
"Downloading model artifact"
);
lastLoggedPct = pct ?? -1;
lastLoggedBytes = downloadedBytes;
lastLoggedAt = Date.now();
};
logProgress(false);
const progressStream = new Transform({
transform(chunk: Buffer, _encoding, callback) {
downloadedBytes += chunk.length;
logProgress(false);
callback(null, chunk);
},
});
try {
await pipeline(nodeStream, progressStream, createWriteStream(tmpPath));
logProgress(true);
await pipeline(nodeStream, createWriteStream(tmpPath));
await rename(tmpPath, outputPath);
} catch (error) {
await rm(tmpPath, { force: true }).catch(() => undefined);
@@ -194,9 +123,6 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption
await downloadToFile({
url: spec.archiveUrl,
outputPath: archivePath,
logger,
modelId: options.modelId,
artifact: archiveFilename,
});
}
@@ -251,9 +177,6 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption
await downloadToFile({
url: file.url,
outputPath: dst,
logger,
modelId: options.modelId,
artifact: file.relPath,
});
}
+58
View File
@@ -0,0 +1,58 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, "..");
const rootPackagePath = path.join(rootDir, "package.json");
function run(cmd, args) {
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
}
function runQuiet(cmd, args) {
return execFileSync(cmd, args, { cwd: rootDir, encoding: "utf8" }).trim();
}
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
if (!version) {
throw new Error('Root package.json must contain a valid "version"');
}
const tag = `v${version}`;
const headCommit = runQuiet("git", ["rev-parse", "HEAD"]);
let localTagCommit = "";
try {
localTagCommit = runQuiet("git", ["rev-list", "-n", "1", tag]);
} catch {
run("git", ["tag", "-a", tag, "-m", tag]);
localTagCommit = runQuiet("git", ["rev-list", "-n", "1", tag]);
}
if (localTagCommit !== headCommit) {
throw new Error(
`Local tag ${tag} points to ${localTagCommit}, but HEAD is ${headCommit}. ` +
"Create a new release commit before pushing this tag."
);
}
run("git", ["push", "origin", "HEAD"]);
let remoteTagExists = false;
try {
runQuiet("git", ["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${tag}`]);
remoteTagExists = true;
} catch {
remoteTagExists = false;
}
if (remoteTagExists) {
console.log(`Tag ${tag} already exists on origin`);
} else {
run("git", ["push", "origin", tag]);
}
console.log(`Release push complete: branch HEAD and tag ${tag}`);