| name | shipwright-deploy |
| description | Deploy to Jelastic (Infomaniak) with smoke test verification, rollback support, and Supabase migrations.
TRIGGER when: user wants to deploy, push to production, deploy to dev, deploy to staging, publish the application, rollback a deployment, or check deployment status.
DO NOT TRIGGER when: user asks to write code (/shipwright-build), run tests (/shipwright-test), fix a bug (/shipwright-iterate), create a changelog (/shipwright-changelog), create requirements (/shipwright-project), plan implementation (/shipwright-plan), or design UI (/shipwright-design). |
| license | MIT |
| compatibility | Requires uv (Python 3.11+), JELASTIC_TOKEN env var, optionally Supabase CLI |
Shipwright Deploy Skill
Deploys to Jelastic (Infomaniak) with smoke tests and rollback.
CRITICAL: First Actions
Governing rules: Read and follow shared/constitution.md (ALWAYS / ASK FIRST / NEVER boundaries).
A. Print Intro Banner
================================================================================
SHIPWRIGHT-DEPLOY: Deployment
================================================================================
Deploys to Jelastic Cloud (Infomaniak, Switzerland).
Usage: /shipwright-deploy (DEV, automatic)
or: /shipwright-deploy --prod (PROD, requires confirmation)
or: /shipwright-deploy --rollback (restore last PROD snapshot)
or: Invoked by /shipwright-run (orchestrator)
Flow:
1. Validate credentials
2. Run Supabase migrations (if applicable)
3. Deploy via Jelastic API
4. Smoke test
5. Rollback on failure
Environments:
DEV: dev-{project}.jpc.infomaniak.com
PROD: {project}.jpc.infomaniak.com
================================================================================
B. Validate Credentials
uv run "{plugin_root}/scripts/checks/validate-deploy.py"
Checks for:
JELASTIC_TOKEN environment variable
- Optionally:
SUPABASE_ACCESS_TOKEN (for migrations)
- Optionally: git repo with remote (for git-based deploy)
B2. Detect Invocation Mode
Resolve it with {shared_root}/scripts/tools/get_phase_context.py --phase-task-id "{phaseTaskId}" --phase deploy (omit --phase-task-id entirely if the orchestrator did not hand you one โ that selects standalone) and store the returned mode as invocation_mode. The dispatch token is the authority โ never re-derive the mode from run-config state (authority: shared/scripts/lib/phase_invocation_mode.py).
pipeline โ dispatched: enforce gates and deploy. Do NOT call orchestrator.py update-step (single-session-apply owns completion; it is inert in a driven run anyway).
standalone โ no token: skip pipeline-state updates; skip the test gate but warn "No pipeline test results found. Deploying without test verification."; still produce all artifacts. If requires_out_of_sequence_warning is true, warn + ASK before continuing (gate deploy.out-of-sequence-continue).
error (exit 2) โ dispatched with an unresolvable token: STOP, return ok: false; never continue as standalone.
Note: a PROD deploy is ASK-FIRST regardless of invocation_mode (constitution).
Single-Session Gate Discipline
Under single-session pipeline mode (run_config.mode == "single_session"), interactive gates follow a per-gate policy โ resolve via ${SHIPWRIGHT_PLUGIN_ROOT}/../../shared/scripts/tools/resolve_gate_policy.py --phase deploy --list. PROD / destructive-migration / rollback gates stay hard-stop (explicit human confirmation, always, regardless of autonomy). Full contract: shared/prompts/single-session-gate-discipline.md.
B3. Validate Environment
Check that required deploy environment variables from the stack profile are available.
uv run "{shared_root}/scripts/validate_env.py" \
--project-root "{project_root}" \
--phase deploy
Where {shared_root} = {plugin_root}/../../shared (relative to plugin root).
Parse the JSON output:
-
skipped == true: No profile or no vars defined โ continue.
-
success == true: All required deploy vars present โ continue.
-
success == false: Missing required vars โ use AskUserQuestion:
Missing environment variables for deployment
The following required variables are not set:
Please set the missing environment variables, then confirm to continue.
Options: "I've set the variables โ continue" / "Skip validation and proceed anyway"
If user updates: re-run validation to confirm.
If user skips: proceed with a warning.
-
optional_missing: Log a warning but do not block.
B4. Verify Tests Passed (MANDATORY)
Before deploying, verify all tests passed:
- Read
shipwright_test_results.json
- Check:
unit.status == "passed" AND (e2e.status == "passed" OR e2e.status == "skipped")
- If tests failed or file does not exist:
================================================================================
SHIPWRIGHT-DEPLOY: Test Gate Failed
================================================================================
Cannot deploy โ tests have not passed.
Unit: {status} | E2E: {status}
Run /shipwright-test first, or confirm to proceed at your own risk.
================================================================================
Ask user for confirmation before proceeding. Do NOT deploy silently with failing tests.
C. Determine Target
| Flag | Target | Behavior |
|---|
| (none) | DEV | Automatic, no confirmation |
--prod | PROD | Requires explicit user confirmation |
--rollback | PROD | Restore last clone, requires confirmation |
Step 0: Phase Session Context Recovery
If the orchestrator handed you a phaseTaskId โ i.e. /shipwright-run dispatched
you as a phase-runner subagent โ you are part of an active pipeline. Run this as your
very first action:
uv run "${SHIPWRIGHT_PLUGIN_ROOT}/../../shared/scripts/tools/get_phase_context.py" \
--phase-task-id <phaseTaskId-from-context>
The tool prints structured JSON with runId, phase, splitId, prerequisites,
runConditions, and a skill_artifacts_to_read list. Read those artifacts
before proceeding so this phase session has full context for what came before.
Deploy is the pipeline-terminal phase โ when this session's Stop hook fires
complete-phase-task, the run will flip to status="complete".
If NO phaseTaskId was handed to you, this is a standalone invocation โ
continue with Step 1 below as normal.
One resolver, one verdict. This is the same tool your "Detect Invocation Mode" step
already ran, so reuse that payload rather than re-deriving anything: its mode IS your
invocation_mode. Pass --phase <your phase> so a token belonging to another phase is
rejected, and if mode is "error" (exit 2) STOP โ a dispatched phase must never
fall back to standalone.
Step 1: Migrations (if applicable)
Only runs if migration files exist in the profile's migrations.dir.
Read migrations config from the stack profile.
Prerequisites (check before running migrations)
supabase/config.toml exists โ if not: run npx supabase init
- Project is linked (
.supabase/ directory exists) โ if not: run npx supabase link --project-ref <ref> (requires SUPABASE_ACCESS_TOKEN)
SUPABASE_ACCESS_TOKEN is set โ if not: prompt user to create one at https://supabase.com/dashboard/account/tokens and add to .env.local
If any prerequisite fails: stop and inform user with specific remediation steps.
Verify DEV migrations
DEV migrations are applied during Build/Iterate. Verify all are current:
{migrations.list_cmd}
If pending migrations exist:
- If
supports_idempotent_apply is true: warn user, offer to apply them now
- If false or unknown: warn user, require explicit confirmation before applying
PROD
{migrations.dry_run_cmd}
Present dry-run output to user (note: dry-run output format varies by stack โ present raw output for human review). Require explicit confirmation before:
{migrations.apply_cmd}
Destructive changes (detected by shipwright-build hooks): always warn and require confirmation regardless of target.
Post-Apply Verification
After apply_cmd succeeds, run the migration verifier against the migrations that were just applied. The verifier parses -- VERIFY: comments from each migration and runs them via psql. A failed verification triggers the same rollback path as a smoke-test failure (see Step 5 โ "Smoke Test Failed โ Rollback").
uv run "{plugin_root}/scripts/lib/migration_verifier.py" \
--migration {applied_migration_path_1} \
[--migration {applied_migration_path_N}] \
--db-url "{prod_db_url_or_pooled_url}" \
--output .shipwright/deploy/migration-verify.json
Read the JSON output. Branch on all_passed:
true โ proceed to "Post-Migration Manual Steps".
false โ present the failing report (per-file, per-VERIFY-statement) to the user via AskUserQuestion. Two options:
- Rollback now (recommended) โ fall through to Step 5's clone-restore path immediately.
- Override and continue โ the user must explicitly acknowledge that the verifier is reporting a real schema mismatch the deploy is choosing to ignore. Before proceeding, write an ADR entry to
.shipwright/agent_docs/decision_log.md capturing: the migration file(s), the failing VERIFY statement(s), the user's stated reason for override, and the timestamp. This is mandatory โ a one-click override on a failed PROD VERIFY without an audit trail is exactly the kind of "did anyone notice that?" event compliance reports must surface afterward. Use write_decision_log.py (see Step 9) with title "Override: failed migration verification".
Backwards-compat: migrations without any -- VERIFY: comment are reported as skipped=True, all_passed=True and do not cause a rollback. New migrations should always include at least one -- VERIFY: block โ see shared/templates/rules/migrations.md.template for the convention and examples.
Post-Migration Manual Steps
Check migrations.post_apply_manual_steps from the stack profile. For each entry where trigger_tag matches a migration just applied, inform user via AskUserQuestion with the action and note. Wait for confirmation before proceeding.
Step 2: Pre-Deploy Safety (PROD only)
Goal: Create a rollback point before deploying to PROD.
uv run "{plugin_root}/scripts/lib/jelastic_client.py" clone-env \
--env-name "{prod_env}" \
--clone-name "{prod_env}-backup"
This creates a full clone of the PROD environment. If deployment fails,
we can restore from this clone.
User confirmation:
AskUserQuestion:
question: "Deploy to PRODUCTION ({prod_env}.jpc.infomaniak.com)?"
context: "Backup clone will be created first."
options:
- "Deploy to PROD"
- "Cancel"
Step 3: Deploy
uv run "{plugin_root}/scripts/lib/jelastic_client.py" deploy \
--env-name "{env_name}" \
--branch "{branch}"
This calls the Jelastic VCS Update API to pull the latest code from git.
If environment doesn't exist yet: create it first via create-env.
Step 4: Smoke Test
uv run "{shared_root}/scripts/smoke_test.py" \
--url "https://{env_name}.jpc.infomaniak.com" \
--profile "{shared_root}/profiles/deploy/jelastic.json"
The profile owns the deadline: keep asking every poll_interval_seconds
until the app answers or max_wait_seconds passes. A slow start-up is not a
failed release โ read attempts / waited_ms before concluding anything.
Without --profile it makes a single attempt.
Step 5: Handle Result
Smoke Test Passed
================================================================================
SHIPWRIGHT-DEPLOY: SUCCESS
================================================================================
Target: {DEV | PROD}
URL: https://{env_name}.jpc.infomaniak.com
Status: {status_code} ({response_time}ms)
Migrations: {applied | skipped | N/A}
================================================================================
Record deploy event (captures deployed URL for downstream consumers):
uv run "{shared_root}/scripts/tools/record_event.py" \
--project-root "$(pwd)" \
--type phase_completed \
--phase deploy \
--detail "https://{env_name}.jpc.infomaniak.com"
Phase complete โ update pipeline state:
Deploy runs the Minimum Phase Completion Canon at C1/C2/C3 only. C4 is skipped
(decided in plan) and C5 is skipped โ deployment is operational history
(events.jsonl + phase_history), not a product change; a per-deploy CHANGELOG
[Unreleased] bullet would duplicate the changelog plugin's release block.
: "${SHIPWRIGHT_RUN_ID:=deploy-$(date +%Y%m%d-%H%M%S)-{env_name}}"
export SHIPWRIGHT_RUN_ID
uv run "{shared_root}/scripts/tools/update_build_dashboard.py" \
--project-root "$(pwd)" --phase deploy --detail "Deployed to {url}" \
--session-id "{SHIPWRIGHT_SESSION_ID}"
uv run "{shared_root}/scripts/tools/generate_session_handoff.py" \
--project-root "$(pwd)" --canon-marker --phase deploy \
--reason "deploy to {env_name}: {status}"
uv run "{shared_root}/scripts/tools/append_phase_history.py" \
--project-root "$(pwd)" --phase deploy --run-id "$SHIPWRIGHT_RUN_ID" \
--entry-json '{"target":"{env_name}","url":"{url}","version":"v{version}","outcome":"success"}'
uv run "{plugin_root}/../../plugins/shipwright-run/scripts/lib/orchestrator.py" \
update-step --project-root "$(pwd)" --step deploy --status complete
Reflection โ Capture Deploy Learnings:
If deployment had issues or required adjustments:
- Infra configuration gotchas?
- Environment-specific behavior?
- Rollback insights?
If learnings exist:
- Observations โ append to
.shipwright/agent_docs/conventions.md under ## Learnings
Format: - ({YYYY-MM-DD}) deploy โ {summary}
- Cross-project insights โ save Claude Code feedback/project Memory
If none: skip.
Smoke Test Failed โ Rollback
DEV: git-based. Passing --project-root + --profile is what arms the
stored-data check and names the target's data-rollback strategy.
uv run "{plugin_root}/scripts/lib/rollback.py" \
--env-name "{env_name}" --strategy git --target-ref "{last_known_good_tag}" \
--project-root "$(pwd)" --profile "{shared_root}/profiles/deploy/jelastic.json"
PROD: stop the failed env so the backup clone can take over.
uv run "{plugin_root}/scripts/lib/rollback.py" \
--env-name "{env_name}" --strategy clone --clone-name "{prod_env}-backup"
Read the exit code โ it is the instruction. Full field table in
rollback-strategy.md.
| Exit | Meaning | What you do |
|---|
0 | rolled back (ref_verified says whether the target confirmed it) | log it, report it; if unconfirmed, say so |
1 | refused before contacting the host โ nothing there changed | fix the reason, re-run. Never claim a rollback happened |
3 | started and did not finish | print operator_message verbatim and STOP. Do not retry, do not redeploy, do not continue unattended |
A 1 from the stored-data gate means migrations exist that the older code does
not know. Do not pass --ack-data-drift on the agent's own judgement โ that is
an ASK-FIRST decision about data, so put it to the user.
Log every rollback in .shipwright/agent_docs/decision_log.md, including a
halted one โ an unfinished rollback is the entry that matters most.
================================================================================
SHIPWRIGHT-DEPLOY: FAILED โ ROLLED BACK
================================================================================
Target: {DEV | PROD}
Error: {smoke test error}
Rollback: {ref {tag}, {confirmed|unconfirmed} | stopped, clone not yet active}
Action: Fix the issue and re-deploy
================================================================================
Manual Rollback (--rollback)
When invoked with --rollback:
- List available backup clones
- Present to user for selection
- Require explicit confirmation
- Stop the failed environment (this does not restore anything โ
restored
is false and the remaining steps are stated)
- Run smoke test on the environment that is now serving
Rollback-Discipline (Universal)
Shipwright treats rollback as a property of every deploy target, not a
feature of one. Three patterns apply universally; their mechanics are
target-specific. The Jelastic flow above is one reference implementation โ
the same discipline applies to any target Shipwright would call shipped.
| Pattern | The property (mechanics are target-specific) |
|---|
| 1 โ Revertable Deploys | A deploy is not complete until its rollback is operable, and the rollback must actually put the requested version back. Application-tier and data-tier are separate concerns: rollback.data_rollback_strategy says what each target does about stored data that has already moved on. |
| 2 โ Provenance Recorded | Every deploy and every rollback leaves an auditable record before the next change touches the target โ phase_completed events, a phase_history entry, and for rollbacks an ADR naming the cause. The why-it-happened outlives the on-call shift. |
| 3 โ Procedure Documented | Both paths โ automatic (smoke-test-fail) and manual โ must be runnable from the documentation alone. Manual rollback needs explicit confirmation; automatic rollback announces itself. A silent rollback is the failure mode worse than the failure that caused it โ and a rollback that reports success it did not achieve is worse still. |
Full per-target mapping and the conformance checks:
rollback-discipline.md.
How discipline becomes target
A target proves it satisfies the discipline by filling in a Deploy Profile
at shared/profiles/deploy/<target_id>.json, validated against
shared/profiles/deploy-profile.schema.json. Three reference profiles
ship today: Jelastic (full implementation, confidence: verified),
Vercel (declarative stub, confidence: documented), and
Compose-VPS (declarative stub, confidence: documented). The two
stubs exist to keep the schema honest โ they describe how targets with
fundamentally different rollback mechanics (atomic vs. snapshot vs. clone)
fill the same shape. To add a real implementation: write the client, fill
the profile, run validate_deploy_profile.py --strict. See
references/rollback-discipline.md
for the pattern-by-pattern mapping.
Reference Documents