chore: script release tag push and trim daemon log noise
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
Reference in New Issue
Block a user