Skip to content

feat(deployments): add a release phase: commands run once per deploy before cutover - #592

Open
Farahat612 wants to merge 1 commit into
oblien:mainfrom
Farahat612:feat/deploy-release-phase
Open

feat(deployments): add a release phase: commands run once per deploy before cutover#592
Farahat612 wants to merge 1 commit into
oblien:mainfrom
Farahat612:feat/deploy-release-phase

Conversation

@Farahat612

Copy link
Copy Markdown

An app project declares exactly one command (the start command), so
framework release steps had nowhere to run: php artisan migrate --force,
rails db:migrate, manage.py migrate never executed, and a stock Laravel
app's schema had to be bootstrapped by hand through the service terminal
(TODO.md "A generic release phase", adjacent to #231).

Projects (and openship.json) can now declare releaseCommands: string[].
They are frozen onto the deployment's config snapshot: a redeploy or
rollback replays the commands that release declared, not today's project
row: and run in the pipeline after a successful build, before any
cutover: before runDeployPipeline, before domain records, while the
previous version is still running and routed. Each command gets its own
log marker; a non-zero exit or a 10-minute per-command timeout fails the
deploy with the command's output, leaving the old version serving. An
absent field deploys byte-identically to before.

Execution per runtime: docker runs each command in a throwaway container
off the freshly built image: not an exec into the running (old-image)
deployment: with the deploy's env, scoped volume binds and project
network, but no published port and no restart policy, removed on every
path. Bare runs in the staged artifact dir through the same login-shell
wrap the build uses, with the start command's env. Compose, cloud and
static deploys log a clear warn-and-skip naming the commands rather than
silently dropping them.

Schema: nullable project.release_commands jsonb (no default, no
backfill), migration 0098 following the volumes pattern; parse/schema
support in openship.json with the published JSON Schema and docs updated
(Laravel/Rails/Django examples). Dashboard UI intentionally deferred.

Tests at every seam: openship-config parsing, snapshot carry in
build.service, phase ordering/failure wiring in the pipeline, and
runtime-level non-zero-exit/timeout/output-capture for docker and bare.
Each was verified to fail with its behavior reverted.

Implements the release-phase half of #231, per the design in TODO.md
("A generic release phase"). Proposed in a comment on that issue.

Design summary:

  • One insertion point in executeServerDeploy, right after deployConfig
    assembly: before runDeployPipeline, before any domain row. A failed
    migration aborts with the previous version still running and routed,
    nothing to unwind.
  • Docker runs each command in a throwaway container off the freshly built
    image (an exec into the running deployment would run new migrations inside
    the old image), with the deploy's env, scoped volume binds and project
    network, but no published port and no restart policy, force-removed on
    every path. Bare runs in the staged artifact dir through the same
    login-shell wrap the build uses. Compose, cloud and static deploys log a
    clear warn-and-skip naming the commands.
  • Commands are frozen on the deployment snapshot: redeploys and rollbacks
    replay that release's commands, and an absent field is byte-identical to
    today (tested).
  • 10-minute per-command budget (same as a bare build step); the timeout is
    checked before the exit code so a kill's 143 is not reported as the
    failure.

Deliberately deferred: dashboard UI field, per-service compose commands,
multi-role stacks (this issue's worker/scheduler half), auto-injected
framework defaults, #206 hooks.

Open design points where I'd take your preference (each is easy to flip):
whether rollback replays the phase; sh -c string vs argv; full env parity
including secrets; bare ordering vs linkPersistentPaths; warn-vs-refuse on
cloud.

Rebased onto current main; the migration is numbered 0107 so it sits after
0106_mcp_call_tracking.

How verified: tests at every seam (openship-config parsing, snapshot carry,
pipeline ordering and failure wiring, runtime-level non-zero-exit, timeout,
and output capture for docker and bare), each demonstrated to fail with its
behavior reverted. On the current base: packages/db 128 tests, the
release-phase suites, adapters and core suites all green.

…before cutover

An app project declares exactly one command (the start command), so
framework release steps had nowhere to run: php artisan migrate --force,
rails db:migrate, manage.py migrate never executed, and a stock Laravel
app's schema had to be bootstrapped by hand through the service terminal
(TODO.md "A generic release phase", adjacent to oblien#231).

Projects (and openship.json) can now declare releaseCommands: string[].
They are frozen onto the deployment's config snapshot: a redeploy or
rollback replays the commands that release declared, not today's project
row: and run in the pipeline after a successful build, before any
cutover: before runDeployPipeline, before domain records, while the
previous version is still running and routed. Each command gets its own
log marker; a non-zero exit or a 10-minute per-command timeout fails the
deploy with the command's output, leaving the old version serving. An
absent field deploys byte-identically to before.

Execution per runtime: docker runs each command in a throwaway container
off the freshly built image: not an exec into the running (old-image)
deployment: with the deploy's env, scoped volume binds and project
network, but no published port and no restart policy, removed on every
path. Bare runs in the staged artifact dir through the same login-shell
wrap the build uses, with the start command's env. Compose, cloud and
static deploys log a clear warn-and-skip naming the commands rather than
silently dropping them.

Schema: nullable project.release_commands jsonb (no default, no
backfill), migration 0107 following the volumes pattern; parse/schema
support in openship.json with the published JSON Schema and docs updated
(Laravel/Rails/Django examples). Dashboard UI intentionally deferred.

Tests at every seam: openship-config parsing, snapshot carry in
build.service, phase ordering/failure wiring in the pipeline, and
runtime-level non-zero-exit/timeout/output-capture for docker and bare.
Each was verified to fail with its behavior reverted.
@Farahat612
Farahat612 force-pushed the feat/deploy-release-phase branch from 4534dd5 to c82e9ae Compare August 16, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant