rampway
v0.5.15
Published
JS-native Capistrano-style release-directory deployments with Rollbridge integration.
Maintainers
Readme
Rampway Deploy
Rampway is a JavaScript-native release-directory deployment tool: the useful parts of Capistrano, built for Node/Expo/Velocious projects and designed to hand runtime changes to Rollbridge.
Package: rampway
CLI: rampway
Config: rampway.config.mjs
Current MVP
This initial scaffold includes:
- ESM config loading and validation
- deterministic deploy plans
- local release-directory deploys for tests/dogfooding
- linked file/dir checks and symlink creation
- deploy lock acquisition/release
- release metadata (
REVISION,TRESTLE_RELEASE.json) - atomic
currentsymlink publishing - safe cleanup
- rollback to previous or selected release
- JSONL deploy reports at
shared/log/rampway-deployments.jsonl - secret redaction helpers
- basic runtime adapter interface with
noneand Rollbridge validate/deploy/status/logs/recover helpers - optional Rampway-owned authenticated Velocious deployment API at
rampway/velocious
SSH transport command execution supports both local-copy and remote-git deployments. remote-git prepares each release checkout on the target host, so the target needs git and access to the configured repository.
Dedicated SSH credentials can select the deploy-host key without an operator-specific config value:
transport: {
type: "ssh",
credentials: {type: "ephemeral-agent", deployKey: "auto"}
}Rampway checks deploy.key beside the loaded config, then the current user's
standard OpenSSH private-key paths. It reports every checked, missing, found,
and selected path on stderr. See the config reference for explicit overrides,
additional identities, and agent-forwarding considerations.
Guard a recovery deploy against an unexpectedly changed active release with:
rampway production deploy CANDIDATE_FULL_SHA --expected-active-revision ACTIVE_FULL_SHABoth values must be full 40-character hexadecimal commit SHAs. The existing before_deploy and before_lock hooks still run before lock acquisition and remain outside the guard's protection. After Rampway acquires the normal deploy lock, its first locked operation strictly resolves current and reads that release's REVISION. A missing, unsafe, malformed, or mismatched current release fails before after_lock and all candidate/source, task, migration, backfill, runtime, and publication work. This option has no bootstrap semantics, and --force cannot bypass it. Deploys without the option retain the ordinary lifecycle.
Full config reference: docs/config-reference.md Migrating from Capistrano: docs/migration-from-capistrano.md
Docker development environment
Rampway's canonical development environment is one persistent dev service built from the root Dockerfile: digest-pinned Ubuntu 26.04 LTS, exact signed/checksummed NodeSource Node 24, the universal coding/debugging baseline, and the current OpenCode, Codex, Claude Code, and official Kimi Code CLIs. The image is source-independent and installs no project dependencies or Threadwire.
Copy the portable environment template, prepare mount-target directories, start the service, and install locked dependencies inside it:
cp .env.example .env
scripts/prepare-dev-home.sh
docker compose up --build --detach dev
scripts/docker-run.sh npm ciThe complete dedicated development home is mounted at /home/dev; GitHub CLI configuration mounts read-only, and Hermes' shared provider runtime supplies Codex, OpenCode, and Kimi authorization through UID/GID-1000 bootstrap links. scripts/docker-run.sh executes project commands through the already-running service. Use a distinct COMPOSE_PROJECT_NAME and DEV_HOME_PATH for each concurrent instance.
Setup, credential boundaries, isolation, static checks, and Docker-capable coordinator validation are documented in docs/docker-development-environment.md.
Commands
rampway init
rampway production validate
rampway production plan
rampway production plan <revision>
rampway production plan --json
rampway production deploy
rampway production deploy <revision>
rampway production deploy --transport=auto
rampway production deploy --transport=http
rampway production deploy --transport=ssh
rampway production status
rampway production releases
rampway production releases --json
rampway production task cache:warm
rampway production task cache:warm --json
rampway production tasks
rampway production tasks --json
rampway production rollback
rampway production doctor
rampway production doctor --json
# Runtime maintenance for the current release
rampway production runtime-validate
rampway production runtime-status
rampway production runtime-logs
rampway production runtime-recover --yes
# Local foreground host-restart entrypoint for the last accepted release
rampway production runtime-boot --config /absolute/path/rampway.config.mjsThe older rollbridge-validate, rollbridge-status, rollbridge-logs, and
rollbridge-recover names remain aliases for Rollbridge stages.
Deploy transports
Deploys run over one of three transports selected with --transport:
http— run the deploy through the authenticated Velocious control plane (requiresstage.http). Fails if the stage has no control-plane config.ssh— run directly over the stage's SSH (or local) transport, bypassing the control plane.auto(default) — use the control plane when the stage configuresstage.http, and fall back to the direct transport only when the control plane is unreachable or the project/stage is not deployed there. Never falls back on authentication or validation errors, or once a run has been admitted.
Dry runs always use the direct transport and print a plan, regardless of
--transport.
Live deploy output and automation
A normal deploy reports real lifecycle steps and remote command output as it arrives, then ends with a concise result:
$ rampway production deploy
→ Preparing release directory
→ Installing dependencies
$ npm ci --omit=dev
… command output …
✓ Deployed production revision 4f31c21 to /home/deploy/app/releases/20260715094500Use --json when another program consumes the result. It suppresses lifecycle and command streaming and writes exactly one final JSON report:
rampway production deploy --json > deploy-report.jsonFor local development without installing the package globally:
node src/cli.js init
node src/cli.js local validate --config examples/local/rampway.config.mjs
node src/cli.js local plan --config examples/local/rampway.config.mjs
node src/cli.js local deploy --config examples/local/rampway.config.mjs
# Monorepo example with backend + frontend stages
node src/cli.js local validate --config examples/monorepo/rampway.config.mjs
node src/cli.js local plan --config examples/monorepo/rampway.config.mjsConfig example
Reusable Node package install helpers can be imported from rampway/tasks when writing rampway.config.mjs:
import {npmInstall, pnpmInstall, checkExpoWebArtifacts, velociousMigrate, onlyIfChanged} from "rampway/tasks"// rampway.config.mjs
export default {
application: "routergeist",
stages: {
production: {
repo: "[email protected]:kaspernj/routergeist.git",
branch: "master",
deployTo: "/home/dev/routergeist",
strategy: "remote-git",
transport: {type: "ssh"},
hosts: [{host: "server.example", user: "dev", roles: ["app", "web", "db"]}],
keepReleases: 5,
linkedFiles: [
{path: "backend/src/config/secrets.js", required: true}
],
linkedDirs: ["log", "tmp/pids", "tmp/cache", "storage"],
tasks: {
install: [
npmInstall({cwd: "backend", production: true}),
pnpmInstall({cwd: "app", production: true})
],
migrate: [
velociousMigrate({cwd: "backend", env: {NODE_ENV: "production"}})
],
build: [
"cd app && npm run web:export",
checkExpoWebArtifacts({dir: "app/dist"}),
{command: "npm run assets:verify", cwd: "app", env: {NODE_ENV: "production"}}
]
},
operationalTasks: {
"cache:warm": {
command: "npm run cache:warm",
cwd: "backend",
env: {NODE_ENV: "production"},
forwardEnv: ["CACHE_WARM_CREDENTIAL"]
}
},
runtime: {
type: "rollbridge",
command: "npx rollbridge",
packageDir: "backend",
config: "config/rollbridge.production.mjs",
daemonLogPath: "/home/dev/routergeist/shared/log/rollbridge.log",
daemonPidPath: "/home/dev/routergeist/shared/tmp/pids/rollbridge.pid",
socketPath: "/tmp/rollbridge-routergeist.sock",
versionPath: "/home/dev/routergeist/shared/tmp/rollbridge-version"
}
}
}
}For Rollbridge, an explicit runtime.config is resolved relative to
runtime.packageDir, fingerprinted with the Rollbridge package version,
runtime options, and environment values, then copied into immutable
Rampway-owned material under shared/rampway/runtime/. After runtime and health
succeed, Rampway atomically records only the known-good digest and secret-free
activation locators in shared/rampway/state.json, indexed by release. Secret
environment values participate in change detection but are never stored or
logged. Keep the Rollbridge config self-contained; imported sibling files are
not copied.
That stable state lets rollback and runtime maintenance target a retained release
whose checkout has no Rampway helper or Rollbridge config. packageDir remains
relative to the selected application release, while the stored config comes from
trusted known-good material. A missing, malformed, mismatched, or config-less
Rollbridge state fails rollback before runtime or current changes. The first
successful Rampway deploy with an explicit config seeds this state.
After a configured Rollbridge handoff and all required health checks succeed,
Rampway also atomically writes shared/rampway/runtime-boot.json. This
versioned, integrity-protected record contains only application/stage identity,
the exact retained release and revision, the immutable config path and digest,
and its acceptance time. It contains no runtime options, environment, commands,
credentials, or output. runtime-boot is local-only and verifies that record,
the retained release metadata, REVISION, current, and immutable config bytes
before spawning Rollbridge 0.1.17 or newer in the foreground using:
rollbridge daemon --config <absolute-config> --release-path <absolute-release> --release-id <id> --revision <revision>The command never deploys or selects a fallback release. Missing, corrupt,
stale, mismatched, or config-less state fails closed. A service manager should
supervise rampway; Rampway does not supervise Rollbridge.
For a persistent external foreground owner, opt in with
runtime.externalOwner: {type: "foreground"} (Rollbridge 0.1.20+). Optional
handoffTimeoutMs and pollIntervalMs positive integers bound the transition.
This mode requires an explicit runtime.config so Rampway can persist stable
foreground boot material before any ownership transition.
Each deploy or rollback must establish the newly accepted boot attestation. If
the current foreground owner already reports that exact identity, it remains
untouched; otherwise, after acceptance and persistence Rampway binds a
handoff request to the active deploy lock and waits for Rollbridge's attested
takeover to return with the exact
release, runtime, and accepted boot digest attestation. Configure the service
manager with Restart=always and a bounded restart delay so early socket
collisions retry and converge; Restart=on-failure cannot recover a clean old
owner exit. Rampway does not invoke the service manager.
npmInstall, pnpmInstall, yarnInstall, bunInstall, and nodePackageInstall return normal task entries with the right install command for common Node package managers. They accept cwd, env, optional, production, and lockfile options such as clean: false for npm install instead of npm ci, or frozenLockfile: false for package managers that need explicit CI lockfile opt-outs (--no-frozen-lockfile for pnpm and --no-immutable for Yarn). Yarn production installs are intentionally not guessed; use a custom task command when that project needs a specific Yarn workspace/plugin flow.
checkExpoWebArtifacts returns a task entry that fails if the Expo web build directory does not contain an index.html. It accepts dir (default "dist"), cwd, and optional.
velociousMigrate returns a task entry that runs npx velocious db:migrate. It accepts command (override), cwd, env, and optional.
onlyIfChanged(paths, task) wraps a task entry so it is skipped when none of the given relative paths have changed since the previous deploy. On the first deploy (no previous release), all tasks run regardless.
Task entries can be strings or objects.
command— required shell command to run.cwd— optional release-relative working directory.env— optional environment variables passed to the command.optional— whentrue, the deploy records the task failure and continues.
Task groups run in config order. Required task failures stop the deploy before publish. If a candidate runtime handoff starts and then fails before publish, Rampway reactivates and health-checks the previous release using that release's known-good snapshot. Deploy failures include whether current changed and recovery hints for the affected stage, such as rerunning a before-publish failure, inspecting a newly created failed release directory, or inspecting rampway status, rampway releases, and rampway rollback after a failure that happened after publish. Existing release-id collisions do not suggest deleting the release directory.
Rollback runs under the same exclusive lock as deploy but never runs deploy task
groups, migrations, or candidate-local Rampway helpers. Rampway resolves one
retained release directory, activates it with the installed runtime adapter,
runs the configured health checks, and only then publishes current. If target
activation or health fails, Rampway reactivates and health-checks the prior
release before returning the original failure. Use --to <exact-release-id> for
a retained Capistrano release. Unsafe, nested, temporary, escaping, and
outside-symlink targets are rejected.
Release diagnostics include safe retained legacy directories. Cleanup ownership
does not: only releases carrying valid Rampway metadata for the configured
application and stage count toward keepReleases or can be removed by Rampway.
Standalone operational tasks are configured separately under operationalTasks and run only when explicitly selected with rampway <stage> task <exact-name>. Rampway acquires the shared deploy lock, strictly resolves the active release, and runs every required entry against that immutable release path. forwardEnv names caller-supplied environment variables that must be present; their values are transported separately from ordinary env and redacted from output and errors. Operational tasks do not run during deploy, accept no command arguments or --force, and are not included in the plural tasks deploy-task inventory.
Callable deployments from Velocious
Velocious applications can mount the authenticated control plane exported as
rampway/velocious. The HTTP API accepts only allowlisted Rampway config/stage
targets, full Git commit SHAs reachable from an approved release branch, and
idempotency keys. Each admitted run is handed to a detached package-owned Node
worker, which reconstructs the backend Velocious context and executes Rampway's
existing deploy/rollback/status lifecycle; matching live revision/fence
requests coalesce onto that canonical worker, and a revision already active
under the deploy-host lock returns an explicit successful no-op before release
or runtime lifecycle work. Velocious remains the framework and persistence
runtime. See
Velocious callable deployment API for
mounting, client calls, durable recovery behavior, and token
rotation/revocation.
Deployment reports
Every successful deploy or rollback appends one JSON object per line to shared/log/rampway-deployments.jsonl. Deploy events include:
event: "deploy"application,stage,releaseId,revision, andbranchdeployedAtanddurationMspreviousReleasehealthChecks: "passed"- sanitized runtime status
cleanupRemovedrelease IDs
Rollback events include event: "rollback", target releaseId and revision,
rolledBackAt, durationMs, previousRelease, healthChecks: "passed", and a
sanitized runtime result. Reports intentionally record runtime result summaries,
not runtime configuration or secrets.
When a remote-git deploy finds the requested revision already active under the
deploy lock, it appends the same bounded deploy event with idempotent: true,
the active releaseId and canonical revision, and healthChecks: "unchanged"
— no runtime result or health success is invented for work that did not run.
Releasing
Patch releases are cut synchronously from a clean checkout with publish access to npm:
npm run release:patchThe release script runs Rampway's lint, typecheck, and test suite before starting
the shared release-patch command. That command syncs master, derives the next
patch version from the latest published annotated vX.Y.Z tag, installs
dependencies, builds, and checks the package with a publish dry run. It then
commits the version files, creates an annotated tag, atomically pushes master
and that tag, publishes to npm, and verifies the published version before
returning. The Git push is atomic, but npm publication is a subsequent operation
and cannot be rolled back by that push. The command requires an npm login with
publish rights; publishing is not delegated to GitHub Actions.
If publishing fails after the commit and tag were pushed, inspect the failure and
resume that exact tagged release with npx release-patch --resume. The release
tool supports patch releases only; minor and major releases require a separately
defined process.
Runtime adapters
Rampway resolves runtimes through a small adapter registry. Built-in adapters are
compose, none, and rollbridge; future adapters can register a runtime type without
changing the deploy lifecycle core. Runtime adapters implement plan() and
deploy(...). Optional prepare(...) and commit(...) methods participate in
secret-free known-good state, and optional validate, status, logs, and
recover methods are exposed by the generic runtime commands.
Rollbridge runtime options:
command— shell command used to invoke Rollbridge, defaulting tonpx rollbridge.packageDir— release-relative directory where Rollbridge commands run.config— optional Rollbridge config path passed as--config.daemonLogPath,daemonPidPath, anddaemonStartTimeoutMs— optional daemon flags forwarded torollbridge deploy.versionPath— optional shared file where Rampway records the deployed Rollbridge package version.socketPath— optional Rollbridge socket removed after a version-change shutdown if a stale daemon leaves it behind.
When versionPath is configured, Rampway reads the release's installed Rollbridge package version before deploy. If it differs from the recorded version and the current runtime is responsive, Rampway shuts down the current Rollbridge daemon, removes configured stale daemon artifacts, deploys the new release, and records the new version.
Compose runtime
Use runtime: {type: "compose", composeRoot, handoffCommand, healthCommand}
when an application is managed by a trusted, fixed Docker Compose deployment.
Rampway executes the static handoff command and then the static health command
from the absolute composeRoot. It records only a digest and the root in
known-good runtime state, never command text or secrets. See the
config reference for the strict field
contract and rollback behavior.
Safety model
Rampway prepares a complete release before flipping current. If a deploy fails before publish, current remains unchanged. Rollbridge runtime deploys are validated and health-checked with rollbridge status before current is updated. Cleanup never removes the current release. Rollback changes current to a previously prepared release and records a deployment event.
