feat(deployments): add a release phase: commands run once per deploy before cutover - #592
Open
Farahat612 wants to merge 1 commit into
Open
feat(deployments): add a release phase: commands run once per deploy before cutover#592Farahat612 wants to merge 1 commit into
Farahat612 wants to merge 1 commit into
Conversation
…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
force-pushed
the
feat/deploy-release-phase
branch
from
August 16, 2026 10:02
4534dd5 to
c82e9ae
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
assembly: before runDeployPipeline, before any domain row. A failed
migration aborts with the previous version still running and routed,
nothing to unwind.
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.
replay that release's commands, and an absent field is byte-identical to
today (tested).
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.