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 ## 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 ```bash
# 1) bump all workspaces and refresh workspace links # Recommended: full patch release (bump, check, publish, push branch+tag)
npm run version:all:patch 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 npm run release:check
# 3) publish relay/server/cli packages
npm run release:publish npm run release:publish
npm run release:push # pushes HEAD and current version tag (triggers desktop release)
``` ```
Notes: 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. - 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 ## 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`). 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 ```bash
git tag v0.1.0 npm run version:all:patch
git push origin v0.1.0 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`). 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", "dev:desktop": "npm run dev --workspace=@getpaseo/desktop",
"build:desktop": "npm run build --workspace=@getpaseo/desktop", "build:desktop": "npm run build --workspace=@getpaseo/desktop",
"cli": "npx tsx packages/cli/src/index.js", "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", "version:sync-internal": "node scripts/sync-workspace-versions.mjs",
"release:prepare": "npm install --workspaces --include-workspace-root", "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:patch": "npm version patch --workspaces --include-workspace-root --message \"chore(release): cut %s\"",
"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:minor": "npm version minor --workspaces --include-workspace-root --message \"chore(release): cut %s\"",
"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: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: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: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": { "devDependencies": {
"concurrently": "^9.2.1", "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 { createAllClients, shutdownProviders } from "./agent/provider-registry.js";
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js"; import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
import { import {
buildOfferEndpoints,
createConnectionOfferV2, createConnectionOfferV2,
encodeOfferToFragmentUrl, encodeOfferToFragmentUrl,
} from "./connection-offer.js"; } from "./connection-offer.js";
@@ -524,20 +523,6 @@ export async function createPaseoDaemon(
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint; const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh"; 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) { if (relayEnabled) {
const offer = await createConnectionOfferV2({ const offer = await createConnectionOfferV2({
serverId, serverId,
@@ -547,8 +532,6 @@ export async function createPaseoDaemon(
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl }); const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
logger.info({ url }, "pairing_offer"); logger.info({ url }, "pairing_offer");
} else {
logger.info("relay_disabled");
} }
if (relayEnabled) { if (relayEnabled) {
@@ -197,10 +197,10 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
}); });
try { try {
const directConnect = await new Promise<{ endpoints: string[] }>((resolve, reject) => { const sawListeningLog = await new Promise<boolean>((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
proc.kill(); proc.kill();
reject(new Error("timed out waiting for direct_connect log")); reject(new Error("timed out waiting for server listening log"));
}, 15000); }, 15000);
const onData = (data: Buffer) => { const onData = (data: Buffer) => {
@@ -213,16 +213,12 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
reject(new Error("unexpected pairing_offer log when --no-relay is set")); reject(new Error("unexpected pairing_offer log when --no-relay is set"));
return; return;
} }
if (!line.includes("direct_connect")) continue;
try { try {
const parsed = JSON.parse(line) as { msg?: string; endpoints?: unknown }; const parsed = JSON.parse(line) as { msg?: string };
if (parsed.msg !== "direct_connect") continue; if (parsed.msg !== `Server listening on http://0.0.0.0:${port}`) continue;
const endpoints = Array.isArray(parsed.endpoints)
? parsed.endpoints.filter((v) => typeof v === "string")
: [];
if (endpoints.length === 0) continue;
clearTimeout(timeout); clearTimeout(timeout);
resolve({ endpoints }); resolve(true);
return; return;
} catch { } catch {
// ignore // ignore
@@ -243,8 +239,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
}); });
}); });
expect(directConnect.endpoints).toContain(`localhost:${port}`); expect(sawListeningLog).toBe(true);
expect(directConnect.endpoints).toContain(`192.168.1.12:${port}`);
} catch (err) { } catch (err) {
throw new Error( throw new Error(
`failed; stdout so far:\\n${stdoutLines.join("")}\\n\\n${String(err)}` `failed; stdout so far:\\n${stdoutLines.join("")}\\n\\n${String(err)}`
@@ -54,15 +54,15 @@ describe("sherpa model downloader", () => {
).rejects.toThrow(/auto-download/i); ).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 modelsDir = makeTmpDir();
const progressLogs: Array<Record<string, unknown>> = []; const infoMessages: string[] = [];
const loggerWithSpy = { const loggerWithSpy = {
child: () => loggerWithSpy, child: () => loggerWithSpy,
info: (obj?: unknown, msg?: string) => { info: (_obj?: unknown, msg?: string) => {
if (msg === "Downloading model artifact" && obj && typeof obj === "object") { if (typeof msg === "string") {
progressLogs.push(obj as Record<string, unknown>); infoMessages.push(msg);
} }
}, },
error: () => undefined, error: () => undefined,
@@ -90,9 +90,8 @@ describe("sherpa model downloader", () => {
} }
expect(fetchMock).toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalled();
expect(progressLogs.length).toBeGreaterThan(0); expect(infoMessages).toContain("Starting model download");
const final = progressLogs.at(-1); expect(infoMessages).toContain("Model download completed");
expect(final?.modelId).toBe("pocket-tts-onnx-int8"); expect(infoMessages).not.toContain("Downloading model artifact");
expect(final?.pct).toBe(100);
}); });
}); });
@@ -1,7 +1,7 @@
import { createWriteStream } from "node:fs"; import { createWriteStream } from "node:fs";
import { mkdir, rename, rm, stat } from "node:fs/promises"; import { mkdir, rename, rm, stat } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { Readable, Transform } from "node:stream"; import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises"; import { pipeline } from "node:stream/promises";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import type pino from "pino"; import type pino from "pino";
@@ -39,31 +39,13 @@ async function hasRequiredFiles(modelDir: string, requiredFiles: string[]): Prom
return true; return true;
} }
const UNKNOWN_SIZE_PROGRESS_BYTES_STEP = 5 * 1024 * 1024;
const UNKNOWN_SIZE_PROGRESS_MS_STEP = 1000;
type DownloadToFileOptions = { type DownloadToFileOptions = {
url: string; url: string;
outputPath: 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> { async function downloadToFile(options: DownloadToFileOptions): Promise<void> {
const { url, outputPath, logger, modelId, artifact } = options; const { url, outputPath } = options;
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) { if (!res.ok) {
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`); 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 }); await mkdir(path.dirname(outputPath), { recursive: true });
const nodeStream = Readable.fromWeb(res.body as any); 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 { try {
await pipeline(nodeStream, progressStream, createWriteStream(tmpPath)); await pipeline(nodeStream, createWriteStream(tmpPath));
logProgress(true);
await rename(tmpPath, outputPath); await rename(tmpPath, outputPath);
} catch (error) { } catch (error) {
await rm(tmpPath, { force: true }).catch(() => undefined); await rm(tmpPath, { force: true }).catch(() => undefined);
@@ -194,9 +123,6 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption
await downloadToFile({ await downloadToFile({
url: spec.archiveUrl, url: spec.archiveUrl,
outputPath: archivePath, outputPath: archivePath,
logger,
modelId: options.modelId,
artifact: archiveFilename,
}); });
} }
@@ -251,9 +177,6 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption
await downloadToFile({ await downloadToFile({
url: file.url, url: file.url,
outputPath: dst, 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}`);