@broberg/deploy-core

中文文档
0.11.0发布于 2026/11/06MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 1939

Fly Live HMAC incremental content-sync and a Fly.io client (Machines REST + the GraphQL the fleet uses) — the shared deploy execution layer for the broberg.ai fleet.

原始使用文档
文档目录5 节
  1. Deploy-event bus (0.10.0, F033.3)
  2. Fly Live: a server fix reaches apps that already run (0.8.0…
  3. FlyClient
  4. What it does that a hand-rolled client usually does not
  5. Verified against the real Fly API

@broberg/deploy-core

The broberg.ai fleet's shared deploy execution layer. It contains three things:

PartWhat it does
Fly Live (flyLiveDeploy, syncContent, diffManifests, …)HMAC-signed incremental content sync to a Fly app, plus the one-time infra build.
FlyClient (since 0.4.0)The Fly.io API: apps and machines over the Machines REST API, and the GraphQL calls the fleet uses.
createDeployEventBus (since 0.10.0)The in-process deploy-event bus: webhook receivers publish, SSE endpoints subscribe, per orgId:siteId.

It does not contain Cloudflare Pages or GitHub Pages providers (planned in F033.2).

Deploy-event bus (0.10.0, F033.3)

import { createDeployEventBus } from "@broberg/deploy-core";

const bus = createDeployEventBus({
  dispatch: (e) => sendPush(e),          // optional: only for "built" / "errored"
  onError: (err, where) => log.warn(where, err), // default console.warn
});
const off = bus.subscribe(orgId, siteId, (e) => sse.send(e));
bus.publish({ type: "page-build", orgId, siteId, status: "built", url, ts: new Date().toISOString() });
bus.listenerCount(orgId, siteId);
  • One bus per app, from the factory — never a module-level Map, so two buses in one process never hear each other.
  • No originator field. F033.4 planned one for Upmetrics' relay and was closed 30/10: Upmetrics derives the sender from the project itself.
  • Publishing to a site nobody listens to is a no-op. The push hook still fires for a terminal event, because push reaches users who have no tab open.
  • A throwing listener or a failed push is reported via onError, never thrown at the webhook that published, and never swallowed silently (cms's original had an empty catch).
  • Single process. Several instances of one app would need Redis pub/sub; out of scope.

Fly Live: a server fix reaches apps that already run (0.8.0, F033.18)

flyLiveDeploy syncs CONTENT to an app that answers /_icd/health; it never replaced the server code inside the container. A server fix (0.7.0's Cache-Control) therefore reached only apps that happened to be re-provisioned. Now the server reports FLY_LIVE_SERVER_VERSION — a hash of its own code and Dockerfile, so it changes whenever the server does — and flyLiveDeploy takes the infra path when a live app reports a different version. The result says why: { mode: "infra", reason: "server version 1.0.0 ≠ 82b3…" }.

The first deploy after upgrading is a full infra deploy, once per app (every running server reports the old "1.0.0"). After that, deploys are content syncs again. chooseDeployMode(health, expected, forceInfra) is exported if you want to decide yourself.

FlyClient

import { FlyClient } from "@broberg/deploy-core";
const fly = new FlyClient(); // token from FLY_API_TOKEN, or { token }

await fly.listApps("personal");                 // one org
await fly.listAllApps();                        // every org the token sees, all pages
const [m] = await fly.listMachines("my-app");
await fly.updateMachine("my-app", m.id, {       // resize: pass the WHOLE config
  ...m.config,
  guest: { ...m.config.guest!, memory_mb: 512 },
});
await fly.waitForState("my-app", m.id, "started", 120);
const { state, exitCode } = await fly.waitForExit("my-app", jobId);

const { entries, nextToken } = await fly.getMachineLogs("my-app", m.id);        // since 0.6.0
await fly.getMachineLogs("my-app", m.id, { nextToken });                         // only newer lines — poll this to tail
await fly.listVolumes("my-app");                // destroyed ones included — filter on state
await fly.deleteVolume("my-app", "vol_123");     // 0.11.0 — Fly refuses an attached volume (FlyApiError). A lost answer + retry can throw 404 although it IS gone: re-read listVolumes before saying "not deleted"
await fly.updateVolume("my-app", "vol_123", { snapshot_retention: 14, auto_backup_enabled: true }); // 0.11.0 — only the fields you give are sent
await fly.listIps("my-app");                    // 0.9.0 — [{ ip, shared, region, serviceName, createdAt }]; a failure throws, never []
await fly.promQuery("personal", 'sum(fly_instance_memory_mem_available{app="my-app"})');

Also: getApp, createApp, deleteApp, getMachine, createMachine, startMachine, stopMachine, destroyMachine, listOrgs, setSecrets, allocateIp.

What it does that a hand-rolled client usually does not

Each of these was measured — in a consumer's own copy or against the live API (24 Sep 2026) — and each has a test that goes red if it is undone:

  • Retries only what can succeed on a second try: network errors, 408, 429, 5xx. A 401, 403, 404 or 422 throws at once with FlyApiError { status, method, path }. (A copy that retried everything turned a wrong token into 30 minutes of polling.)
  • waitForExit reports exitCode: null when Fly recorded none. It does not call that a success; you decide.
  • waitForExit throws a 404 for a machine it never saw (since 0.5.0). Fly answers 404 "machine not found" both for a wrong id and for a WRONG TOKEN, so a 404 on the first poll is not "destroyed". Only a machine seen in an earlier poll that then disappears (auto_destroy) is reported as destroyed.
  • promQuery hides Fly's Prometheus auth trap (since 0.5.0). Fly's Prometheus answers 401 to Bearer and wants FlyV1 <token>; the Machines API takes Bearer. HTTP 200 with status: "error" throws.
  • waitForState reads Fly's 408 as "not yet". Fly's /wait answers 408 when its own timeout runs out; the client keeps asking until your timeout is spent, then throws FlyTimeoutError.
  • GraphQL HTTP 200 is not success. Fly answers 200 for a bad token with the error in the body; the client throws a FlyApiError with status 401.
  • listAllApps is complete or it throws. It walks every page and checks the count against Fly's own totalCount. A partial list never looks like a whole one — which matters when the caller deletes what is missing from it.
  • getMachineLogs uses Fly's logs API, not the obvious address (since 0.6.0). api.machines.dev/v1/apps/<app>/machines/<id>/logs answers 404 — measured 29/9 2026 — so a hand-rolled poller that swallows errors shows an empty log forever. The real endpoint is api.fly.io/api/v1/apps/<app>/logs?instance=<id> and, like Prometheus, wants FlyV1 <token>. A 200 without a data array throws.
  • The token never appears in an error message or body.

Verified against the real Fly API

test/live/fly-api.live.ts runs read-only against an existing app (compared with flyctl machines list / flyctl volumes list, plus promQuery and the never-seen-machine 404; FLY_LIVE_READ_ONLY=1 stops there) and runs the write path — create app, create machine, resize and read back, stop, a process that exits 3, destroy, delete app — inside a throwaway app it always deletes. Run by hand; it is not part of vitest run.

版本与发布记录

@broberg/deploy-core 共发布 15 个版本,首次发布于 2026/06/30;过去 12 个月发布了 15 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.11.0latest2026/11/063 天前
0.10.02026/11/036 天前
0.9.02026/11/036 天前
0.8.22026/10/2713 天前
0.8.12026/10/2614 天前
0.8.02026/10/2614 天前

关于 @broberg/deploy-core 的常见问题

@broberg/deploy-core 是什么?

Fly Live HMAC incremental content-sync and a Fly.io client (Machines REST + the GraphQL the fleet uses) — the shared deploy execution layer for the broberg.ai fleet.

如何安装 @broberg/deploy-core?

在项目目录运行 npm install @broberg/deploy-core,或使用 pnpm add / yarn add / bun add。

@broberg/deploy-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@broberg/deploy-core 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@broberg/deploy-core 可以商用吗?

@broberg/deploy-core 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@broberg/deploy-core 的下载量有多少?

根据 npm 官方下载统计,@broberg/deploy-core 在 2026/10/09 – 2026/11/07 这 30 天内被下载 1,939 次,日均约 65 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@broberg/deploy-core 还在维护吗?

最新版本 0.11.0 发布于 2026/11/06(3 天前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/06/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。