@molecule/api-deploy-rollout-fly

Provider bond · deploy-rollout · API (Node) · v2.0.1 · Apache-2.0

Fly Machines provider bond for @molecule/api-deploy-rollout: in-place restarts with a restore path and grow-then-shrink clones, over an injected flyctl runner.

npm install @molecule/api-deploy-rollout-fly

npm · Source on GitHub · Implements @molecule/api-deploy-rollout

How it works

@molecule/api-deploy-rollout-fly is a provider bond on the API (Node) side: it implements the deploy-rollout core interface (@molecule/api-deploy-rollout) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { runInPlaceRollout, setProvider } from '@molecule/api-deploy-rollout'
import { provider } from '@molecule/api-deploy-rollout-default'
import { createFlyInPlaceProvider } from '@molecule/api-deploy-rollout-fly'

setProvider(provider)
await runInPlaceRollout({
  provider: createFlyInPlaceProvider({
    app: 'molecule-api',
    isServing: (id) => probeMachine(id),
    restore: (id) => clearPatchAndRestart(id),
  }),
  policy: { soakMs: 30_000 },
})

Works with: @molecule/api-deploy-rollout

Reference

Fly Machines provider bonds for the deploy-rollout core.

In-place (restart the same Machines one at a time, with a restore path) and grow-then-shrink (clone a Machine beside the old set, retire the old one when the new one serves). Commands run through an injected runner, so tests need no Fly account.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { runInPlaceRollout, setProvider } from '@molecule/api-deploy-rollout'
import { provider } from '@molecule/api-deploy-rollout-default'
import { createFlyInPlaceProvider } from '@molecule/api-deploy-rollout-fly'

setProvider(provider)
await runInPlaceRollout({
  provider: createFlyInPlaceProvider({
    app: 'molecule-api',
    isServing: (id) => probeMachine(id),
    restore: (id) => clearPatchAndRestart(id),
  }),
  policy: { soakMs: 30_000 },
})

Type

provider

Installation

npm install @molecule/api-deploy-rollout-fly @molecule/api-deploy-rollout

API

Interfaces

FlyMachine

One Fly Machine, as the rollout needs to see it.

interface FlyMachine extends RolloutInstance {
  /** The Machine's Fly state (`started`, `stopped`, …). */
  state: string
  /** The image the Machine runs, tag or digest form. */
  imageRef: string | null
  /** The Machine's configuration version (`instance_id`/`version`). */
  version: string | null
  /**
   * The longest service-check grace period the Machine declares, in milliseconds.
   *
   * Fly reports a check inside its grace window as PASSING and can route traffic
   * to a Machine that has not booted; a rolling restart must know the number to
   * refuse it. Zero when the Machine's checks have no grace.
   */
  gracePeriodMs: number
}

FlyCommandResult

The result of one flyctl invocation.

interface FlyCommandResult {
  /** Exit status, or `null` when the process did not exit normally. */
  status: number | null
  /** Standard output. */
  stdout: string
  /** Standard error. */
  stderr?: string
}

FlyMachinesOptions

Where and how the adapter reaches one Fly app.

interface FlyMachinesOptions {
  /** The Fly app name. */
  app: string
  /** The runner; defaults to spawning the local `flyctl`. */
  run?: FlyCommandRunner
}

FlyInPlaceOptions

What an in-place Fly rollout supplies that Fly itself does not know.

The adapter knows how to list, restart and start a Machine. Whether a Machine is SERVING, what "replace" means (a patch volume swap, a new image) and how to undo it are properties of the app being deployed, so the caller states them.

interface FlyInPlaceOptions extends FlyMachinesOptions {
  /** Whether a Machine answers its readiness probe right now. */
  isServing: (machineId: string) => Promise<boolean>
  /**
   * Change one Machine to the new build and leave it serving. Defaults to a restart
   * that waits for the Machine to reach `started`.
   */
  replace?: (machineId: string) => Promise<void>
  /** Put one Machine back on the old build. */
  restore?: (machineId: string) => Promise<void>
  /** How long a restart may take to reach `started`. Defaults to 6 minutes. */
  restartBudgetMs?: number
  /** Sleep between state polls. Defaults to 5000 ms. */
  pollMs?: number
}

FlyInstanceOptions

What a grow-then-shrink Fly rollout supplies that Fly itself does not know.

launch builds the Machine (image, volume, env, mounts) — that is the app's deployment recipe. join/leave change whatever edge routes to the app; Fly's own proxy balances by service, not by a caller-held set.

interface FlyInstanceOptions extends FlyMachinesOptions {
  /** Create the Machine for `index` and resolve once it exists. */
  launch: (index: number) => Promise<FlyMachine>
  /** Whether a Machine answers its readiness probe right now. */
  isServing: (machine: FlyMachine) => Promise<boolean>
  /** Add a ready Machine to the caller's routable set. */
  join?: (machine: FlyMachine) => Promise<void>
  /** Remove a Machine from the caller's routable set. */
  leave?: (machine: FlyMachine) => Promise<void>
  /** How long `machine stop` may take. Defaults to 60 s. */
  stopBudgetMs?: number
}

Types

FlyCommandRunner

Runs one flyctl command (without the leading flyctl).

type FlyCommandRunner = (args: string[]) => Promise<FlyCommandResult>

Functions

flyctlRunner(options)

A runner that spawns the local flyctl.

function flyctlRunner(options?: { cwd?: string }): FlyCommandRunner
  • options — The working directory flyctl should see.

Returns: The runner.

parseFlyDurationMs(value)

Parse a Fly duration (500ms, 5s, 1m0s, 1h30m) into milliseconds.

function parseFlyDurationMs(value: unknown): number
  • value — The configured value.

Returns: Milliseconds; 0 when absent or not a duration.

serviceCheckGraceMs(machine)

The longest service-check grace period any of a Machine's checks declares.

function serviceCheckGraceMs(machine: unknown): number
  • machine — One entry from flyctl machine list --json.

Returns: Milliseconds; 0 when the Machine's checks have no grace.

parseFlyMachineList(json)

Parse flyctl machine list --app <app> --json into the facts a rollout needs.

function parseFlyMachineList(json: string): FlyMachine[]
  • json — The command's stdout.

Returns: Every Machine, in the order Fly listed them.

assertHomogeneousFlyMachines(machines)

Validate a Machine list as a rollout target set and return what to act on.

Every Machine must be started (a stopped one cannot be reached over SSH, and its patch volume would keep an older build), and all must run the SAME image. Configuration versions identify individual Machine instances, so a healthy rolling deployment has a different version on each Machine. Preserve those versions by id so callers can bind a patch to the Machine receiving it.

function assertHomogeneousFlyMachines(
  machines: readonly FlyMachine[],
  options?: { appName?: string },
): { ids: string[]; imageRef: string; machineVersions: Record<string, string> }
  • machines — Parsed Machines.

Returns: The target set's ids, shared image ref and each Machine's version.

listFlyMachines(options)

List one Fly app's Machines.

function listFlyMachines(options: FlyMachinesOptions): Promise<FlyMachine[]>
  • options — The app and the runner.

Returns: Every Machine the app has.

restartFlyMachine(options, machineId)

Restart one Machine and wait until it is started again.

A Machine that comes back stopped or suspended is started explicitly — Fly occasionally parks one after a restart, and a role that leaves it parked turns a successful roll into an outage.

function restartFlyMachine(
  options: FlyMachinesOptions & { restartBudgetMs?: number; pollMs?: number },
  machineId: string,
): Promise<boolean>
  • options — The app, the runner and the waits.
  • machineId — The Machine to restart.

Returns: Whether it reached started inside the budget.

createFlyInPlaceProvider(options)

Create an in-place Fly Machines provider for the rollout core.

function createFlyInPlaceProvider(options: FlyInPlaceOptions): InPlaceInstanceProvider<FlyMachine>
  • options — The app, the runner, the readiness probe and the restore path.

Returns: An InPlaceInstanceProvider over Fly Machines.

createFlyInstanceProvider(options)

Create a grow-then-shrink Fly Machines provider for the rollout core.

create delegates to the caller's launch; drain asks Fly to stop the Machine (a SIGTERM with a bounded wait), and destroy removes it.

function createFlyInstanceProvider(options: FlyInstanceOptions): InstanceProvider<FlyMachine>
  • options — The app, the runner and the app-specific operations.

Returns: An InstanceProvider over Fly Machines.

Core Interface

Implements @molecule/api-deploy-rollout interface.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-deploy-rollout ^1.0.0

Runtime Dependencies

  • @molecule/api-deploy-rollout

  • parseFlyMachineList is the only JSON the adapter trusts. A Machine list that is not an array is an error, never an empty set.

  • A mixed set is refused. assertHomogeneousFlyMachines rejects Machines on different images. Each Machine has its own configuration version; the returned machineVersions maps Machine ids to those versions. Bind each patch to its target's version so a later image or configuration update retires it.

  • A restart that parks is started explicitly. A Machine that comes back stopped/suspended after a restart is started, not left parked.