Add dual-runtime deploy workflow
This commit is contained in:
parent
803740190c
commit
4f3df27b20
6 changed files with 798 additions and 102 deletions
|
|
@ -10,6 +10,8 @@
|
||||||
{"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-qh7","title":"Implement dual-runtime deploy workflow with partial deploys","description":"Implement the planned refactor of the root deploy script and scripts/deploy.ts so deployment can target Docker and host-native runtimes during a transition period. Preserve local dev as Docker infra plus native Bun services/web, add explicit runtime selection, runtime-specific prechecks/rollout/verification, and support partial deploy scopes such as web-only or services-only. Update operator documentation for the new workflow.","notes":"Implemented dual-runtime deploy workflow. scripts/deploy.ts now supports --runtime docker|native, scope flags (--web-only, --api-only, --services-only), and --no-build. Docker verification now uses docker compose exec instead of hardcoded container names. Added deployment/native/README.md and updated README.md plus deployment/docker/README.md for the new workflow. Validation: bun run scripts/deploy.ts --help, bun run check:docker-workspace, guard checks for invalid flag combinations.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:38:31Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:46:17Z","started_at":"2026-05-15T23:40:13Z","closed_at":"2026-05-15T23:46:17Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-iiy","title":"Plan deploy workflow changes for Docker/native transition","description":"User requested a repo-specific plan for updating the root deploy script and deployment workflow to support Docker/native transition paths, faster local iteration, and partial deploy modes. This task covers confirming the target workflow, documenting current assumptions, and producing an implementation-ready plan without changing implementation files.","notes":"Confirmed transition strategy: local dev stays Docker-infra-only plus native Bun services/web; VPS deploy path should support both Docker and host-native runtimes during transition; partial deploys are desired; current main/current-branch modes may evolve. Produced an implementation-ready plan covering current assumptions, runtime split, CLI shape, prechecks, rollout, verification, rollback, docs, and validation scenarios. Follow-up implementation tracked in islandflow-qh7.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:37:28Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:38:41Z","started_at":"2026-05-15T23:37:30Z","closed_at":"2026-05-15T23:38:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-wab","title":"Quiet the terminal view chrome","description":"The Islandflow terminal view currently carries too much chrome intensity: strong shell gradients, visible grid texture, active amber wash, glassy overlays, and heavily styled drawer/filter surfaces compete with live data. Refine the product UI so the terminal feels calmer and more forensic while preserving status clarity, scan speed, and identity. Focus on reducing decorative contrast, flattening surfaces, and making accents scarcer without weakening affordances.","notes":"Refined terminal chrome in apps/web/app/globals.css: moved shell tokens to quieter OKLCH values, removed grid texture, flattened panes/overlays, reduced active amber wash, softened classified row treatment, and added reduced-motion handling for the connecting pulse. Validation: bun test apps/web/app/terminal.test.ts; bun --cwd=apps/web run build.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T12:05:25Z","created_by":"dirtydishes","updated_at":"2026-05-15T12:13:10Z","started_at":"2026-05-15T12:05:30Z","closed_at":"2026-05-15T12:13:10Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-wab","title":"Quiet the terminal view chrome","description":"The Islandflow terminal view currently carries too much chrome intensity: strong shell gradients, visible grid texture, active amber wash, glassy overlays, and heavily styled drawer/filter surfaces compete with live data. Refine the product UI so the terminal feels calmer and more forensic while preserving status clarity, scan speed, and identity. Focus on reducing decorative contrast, flattening surfaces, and making accents scarcer without weakening affordances.","notes":"Refined terminal chrome in apps/web/app/globals.css: moved shell tokens to quieter OKLCH values, removed grid texture, flattened panes/overlays, reduced active amber wash, softened classified row treatment, and added reduced-motion handling for the connecting pulse. Validation: bun test apps/web/app/terminal.test.ts; bun --cwd=apps/web run build.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T12:05:25Z","created_by":"dirtydishes","updated_at":"2026-05-15T12:13:10Z","started_at":"2026-05-15T12:05:30Z","closed_at":"2026-05-15T12:13:10Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-hio","title":"Add Pi /plan command for plan mode","description":"Create a Pi extension so typing /plan activates plan mode instructions and guards against implementation file edits until disabled.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T04:56:00Z","created_by":"dirtydishes","updated_at":"2026-05-15T04:57:03Z","started_at":"2026-05-15T04:56:03Z","closed_at":"2026-05-15T04:57:03Z","close_reason":"Implemented project-local Pi /plan extension with plan-mode guardrails.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-hio","title":"Add Pi /plan command for plan mode","description":"Create a Pi extension so typing /plan activates plan mode instructions and guards against implementation file edits until disabled.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T04:56:00Z","created_by":"dirtydishes","updated_at":"2026-05-15T04:57:03Z","started_at":"2026-05-15T04:56:03Z","closed_at":"2026-05-15T04:57:03Z","close_reason":"Implemented project-local Pi /plan extension with plan-mode guardrails.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-t8s","title":"Reconcile merge conflicts on impeccable","description":"Resolve the PR branch conflicts against main while preserving terminal hardening, responsive adaptation, and related test coverage.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-14T22:32:40Z","created_by":"dirtydishes","updated_at":"2026-05-14T22:34:03Z","started_at":"2026-05-14T22:33:05Z","closed_at":"2026-05-14T22:34:03Z","close_reason":"Rebased impeccable onto main, resolved the terminal test conflict, and revalidated the web app.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-t8s","title":"Reconcile merge conflicts on impeccable","description":"Resolve the PR branch conflicts against main while preserving terminal hardening, responsive adaptation, and related test coverage.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-14T22:32:40Z","created_by":"dirtydishes","updated_at":"2026-05-14T22:34:03Z","started_at":"2026-05-14T22:33:05Z","closed_at":"2026-05-14T22:34:03Z","close_reason":"Rebased impeccable onto main, resolved the terminal test conflict, and revalidated the web app.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
@ -30,4 +32,5 @@
|
||||||
{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-zs0","title":"Migrate terminal UI to smart-money profiles","description":"Migrate apps/web terminal rendering to consume SmartMoneyEvent directly: primary profile, probability ladder, reason codes, and suppression/abstention state, while preserving legacy alert/classifier displays during the bridge.","status":"closed","priority":2,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-04T21:35:23Z","created_by":"dirtydishes","updated_at":"2026-05-05T05:39:58Z","closed_at":"2026-05-05T05:39:58Z","close_reason":"Completed terminal smart-money profile migration","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-igk","title":"Add plan mode","description":"Implement a user-facing plan mode in the application so users can switch into planning before taking action. Scope to be clarified from existing app patterns.","status":"closed","priority":2,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-04T04:22:37Z","created_by":"dirtydishes","updated_at":"2026-05-04T04:26:18Z","started_at":"2026-05-04T04:22:40Z","closed_at":"2026-05-04T04:26:18Z","close_reason":"Implemented as a global pi extension toggled with Shift+P","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-biq","title":"Finish raw live options delivery and filter/backpressure observability","description":"The smart-money signal path and Tape filters are in place, but the next firehose pass should finish server-side selective raw live delivery for options subscriptions and add explicit filtered-out/backpressure observability for API/web counters. This was discovered while landing islandflow-e4r.\n","status":"in_progress","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:28:58Z","created_by":"dirtydishes","updated_at":"2026-04-29T03:54:12Z","started_at":"2026-04-29T03:54:12Z","dependencies":[{"issue_id":"islandflow-biq","depends_on_id":"islandflow-e4r","type":"discovered-from","created_at":"2026-04-28T16:28:58Z","created_by":"dirtydishes","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"islandflow-38p","title":"Add native deployment unit templates and rollback helpers","description":"The deploy helper now supports --runtime native, but the repo still relies on operator-managed systemd units and manual rollback. Add checked-in native deployment templates or provisioning guidance for the expected units, and consider lightweight rollback/smoke-test helpers once the host-native path is exercised on the real VPS.","status":"open","priority":3,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:46:42Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:46:42Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"islandflow-575","title":"Document smart-money event calendar env","description":"Document smart-money event-calendar environment configuration in env examples and README.\n","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T06:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-05T06:57:57Z","started_at":"2026-05-05T06:57:17Z","closed_at":"2026-05-05T06:57:57Z","close_reason":"Documented event-calendar env variables","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"islandflow-575","title":"Document smart-money event calendar env","description":"Document smart-money event-calendar environment configuration in env examples and README.\n","status":"closed","priority":3,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-05T06:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-05T06:57:57Z","started_at":"2026-05-05T06:57:17Z","closed_at":"2026-05-05T06:57:57Z","close_reason":"Documented event-calendar env variables","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -116,6 +116,23 @@ Start web only:
|
||||||
|
|
||||||
- `bun run dev:web`
|
- `bun run dev:web`
|
||||||
|
|
||||||
|
Recommended fast iteration loop:
|
||||||
|
|
||||||
|
- `bun run dev:infra` for Docker-backed infra only
|
||||||
|
- `bun run dev:services` for native Bun backend services
|
||||||
|
- `bun run dev:web` for the local Next.js UI
|
||||||
|
|
||||||
|
This keeps Docker in the local workflow where it helps most (NATS, ClickHouse, Redis) without forcing the app services themselves into slower container rebuild/restart loops.
|
||||||
|
|
||||||
|
## Deployment Workflow
|
||||||
|
|
||||||
|
- `./deploy main` keeps the current VPS Docker rollout path as the default.
|
||||||
|
- `./deploy main --runtime native` targets a host-native Bun + systemd deployment.
|
||||||
|
- `./deploy current-branch` and `./deploy current-branch --runtime native` keep branch deploys available during the transition.
|
||||||
|
- Partial deploys are supported with `--web-only`, `--api-only`, `--services-only`, and `--no-build`.
|
||||||
|
- Docker runtime details live in `deployment/docker/README.md`.
|
||||||
|
- Native runtime expectations live in `deployment/native/README.md`.
|
||||||
|
|
||||||
## Desktop Shell
|
## Desktop Shell
|
||||||
|
|
||||||
Islandflow also includes a thin Electron desktop shell in `apps/desktop`.
|
Islandflow also includes a thin Electron desktop shell in `apps/desktop`.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
# Docker Deployment
|
# Docker Deployment
|
||||||
|
|
||||||
This directory is the supported VPS deployment path for Islandflow.
|
This directory contains the Docker runtime for Islandflow VPS deployments.
|
||||||
|
|
||||||
The repo no longer ships or supports a separate `deployment/npm` stack. Docker Compose is the deployment surface; if you want a reverse proxy, point it at the host ports published by this stack.
|
Docker remains the default server rollout path, but the repo-root `deploy` helper can now target either:
|
||||||
|
|
||||||
|
- `--runtime docker` for this Docker Compose stack
|
||||||
|
- `--runtime native` for a host-native Bun + systemd rollout described in `deployment/native/README.md`
|
||||||
|
|
||||||
|
The repo no longer ships or supports a separate `deployment/npm` stack. If you want a reverse proxy, point it at the host ports published by this stack.
|
||||||
|
|
||||||
It is separate from the repo-root `docker-compose.yml`, which remains the lightweight local infra stack for development.
|
It is separate from the repo-root `docker-compose.yml`, which remains the lightweight local infra stack for development.
|
||||||
|
|
||||||
|
|
@ -198,6 +203,7 @@ It preserves the current Docker Compose project and avoids destructive cleanup o
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy main
|
./deploy main
|
||||||
|
./deploy main --runtime docker
|
||||||
```
|
```
|
||||||
|
|
||||||
This flow:
|
This flow:
|
||||||
|
|
@ -213,6 +219,7 @@ This flow:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy current-branch
|
./deploy current-branch
|
||||||
|
./deploy current-branch --runtime docker
|
||||||
```
|
```
|
||||||
|
|
||||||
Alias:
|
Alias:
|
||||||
|
|
@ -229,13 +236,24 @@ This flow:
|
||||||
- switches the server checkout to that same branch and keeps it there until you intentionally move it back
|
- switches the server checkout to that same branch and keeps it there until you intentionally move it back
|
||||||
- runs the same rebuild and verification steps as `main`
|
- runs the same rebuild and verification steps as `main`
|
||||||
|
|
||||||
|
### Partial Docker rollouts
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy main --runtime docker --web-only
|
||||||
|
./deploy main --runtime docker --api-only
|
||||||
|
./deploy current-branch --runtime docker --services-only
|
||||||
|
./deploy main --runtime docker --web-only --no-build
|
||||||
|
```
|
||||||
|
|
||||||
### Escalation path
|
### Escalation path
|
||||||
|
|
||||||
Use force recreate only when a normal refresh does not update the services cleanly:
|
Use force recreate only when a normal refresh does not update the services cleanly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy main --force-recreate
|
./deploy main --runtime docker --force-recreate
|
||||||
./deploy current-branch --force-recreate
|
./deploy current-branch --runtime docker --force-recreate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Return the server to `main`
|
### Return the server to `main`
|
||||||
|
|
@ -244,6 +262,7 @@ If the live checkout is on a branch deploy and you want normal production tracki
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy main
|
./deploy main
|
||||||
|
./deploy main --runtime docker
|
||||||
```
|
```
|
||||||
|
|
||||||
The helper always does the final public verification against:
|
The helper always does the final public verification against:
|
||||||
|
|
|
||||||
122
deployment/native/README.md
Normal file
122
deployment/native/README.md
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Native Deployment
|
||||||
|
|
||||||
|
This directory documents the host-native Islandflow rollout path used by:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy main --runtime native
|
||||||
|
./deploy current-branch --runtime native
|
||||||
|
```
|
||||||
|
|
||||||
|
This runtime is intended for faster server iteration during the transition away from Docker-only app rollouts. Local development should still prefer:
|
||||||
|
|
||||||
|
- Docker for infra (`bun run dev:infra`)
|
||||||
|
- native Bun services (`bun run dev:services`)
|
||||||
|
- native Next.js web (`bun run dev:web`)
|
||||||
|
|
||||||
|
## What native deploy means here
|
||||||
|
|
||||||
|
The checked-in `deploy` helper assumes:
|
||||||
|
|
||||||
|
- the live repo checkout is still `/home/delta/islandflow`
|
||||||
|
- Bun is installed on the VPS
|
||||||
|
- app processes are managed by `systemd`
|
||||||
|
- infrastructure services such as NATS, ClickHouse, and Redis are already reachable from the host
|
||||||
|
- the web app runs from `apps/web` and is served with `next start -p 3000`
|
||||||
|
|
||||||
|
The deploy script updates the repo checkout, optionally runs `bun install --frozen-lockfile`, optionally rebuilds the web app, restarts the target systemd units, and then verifies the services locally on the VPS plus through the public app URL.
|
||||||
|
|
||||||
|
## Expected unit names
|
||||||
|
|
||||||
|
Default unit names used by `scripts/deploy.ts`:
|
||||||
|
|
||||||
|
- `islandflow-web`
|
||||||
|
- `islandflow-api`
|
||||||
|
- `islandflow-compute`
|
||||||
|
- `islandflow-candles`
|
||||||
|
- `islandflow-ingest-options`
|
||||||
|
- `islandflow-ingest-equities`
|
||||||
|
|
||||||
|
Override them from your local shell before running `./deploy` if the server uses different names:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DEPLOY_NATIVE_WEB_UNIT=my-web-unit
|
||||||
|
export DEPLOY_NATIVE_API_UNIT=my-api-unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Available overrides:
|
||||||
|
|
||||||
|
- `DEPLOY_NATIVE_WEB_UNIT`
|
||||||
|
- `DEPLOY_NATIVE_API_UNIT`
|
||||||
|
- `DEPLOY_NATIVE_COMPUTE_UNIT`
|
||||||
|
- `DEPLOY_NATIVE_CANDLES_UNIT`
|
||||||
|
- `DEPLOY_NATIVE_INGEST_OPTIONS_UNIT`
|
||||||
|
- `DEPLOY_NATIVE_INGEST_EQUITIES_UNIT`
|
||||||
|
|
||||||
|
## systemctl invocation
|
||||||
|
|
||||||
|
By default the deploy helper uses:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl
|
||||||
|
```
|
||||||
|
|
||||||
|
If the server uses user units or another wrapper, override it locally before invoking `./deploy`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DEPLOY_NATIVE_SYSTEMCTL_PREFIX="systemctl --user"
|
||||||
|
./deploy main --runtime native
|
||||||
|
```
|
||||||
|
|
||||||
|
## Partial native rollouts
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy main --runtime native --web-only
|
||||||
|
./deploy main --runtime native --api-only
|
||||||
|
./deploy current-branch --runtime native --services-only
|
||||||
|
./deploy main --runtime native --web-only --no-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Scope behavior:
|
||||||
|
|
||||||
|
- default: restart web + API + backend services
|
||||||
|
- `--web-only`: rebuild/restart only the web unit
|
||||||
|
- `--api-only`: restart only the API unit
|
||||||
|
- `--services-only`: restart API + backend units without touching the web unit
|
||||||
|
- `--no-build`: skip `bun install --frozen-lockfile` and skip the web build step
|
||||||
|
|
||||||
|
## Server preparation checklist
|
||||||
|
|
||||||
|
Before the first native rollout, ensure the VPS has:
|
||||||
|
|
||||||
|
1. Bun installed and on `PATH`
|
||||||
|
2. a working `/home/delta/islandflow/.env` (or unit-managed equivalent env source)
|
||||||
|
3. systemd units for each target service
|
||||||
|
4. the web unit configured to serve the built app on port `3000`
|
||||||
|
5. the API unit configured to serve health checks on port `4000`
|
||||||
|
6. infrastructure endpoints configured so the native services can reach NATS, ClickHouse, and Redis
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Native deploys verify:
|
||||||
|
|
||||||
|
- target units are active via `systemctl`
|
||||||
|
- recent unit status and journal output can be collected
|
||||||
|
- local `http://127.0.0.1:4000/health` when API scope is included
|
||||||
|
- local `http://127.0.0.1:3000/` when web scope is included
|
||||||
|
- the public app URL from the local machine after the rollout finishes
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Rollback remains manual for now:
|
||||||
|
|
||||||
|
1. switch the server checkout back to the last known-good branch or commit
|
||||||
|
2. rerun the appropriate native deploy command
|
||||||
|
3. if needed, restart only the affected units with `systemctl`
|
||||||
|
|
||||||
|
Docker remains available as the fallback runtime during the transition:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy main --runtime docker
|
||||||
|
```
|
||||||
170
docs/turns/2026-05-15-dual-runtime-deploy-workflow.html
Normal file
170
docs/turns/2026-05-15-dual-runtime-deploy-workflow.html
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>2026-05-15: Dual-runtime deploy workflow</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #10131a;
|
||||||
|
--panel: #171c25;
|
||||||
|
--panel-2: #1e2531;
|
||||||
|
--text: #e8edf5;
|
||||||
|
--muted: #9fb0c8;
|
||||||
|
--accent: #7cc4ff;
|
||||||
|
--border: #2d3848;
|
||||||
|
--good: #7dd3a7;
|
||||||
|
--warn: #f6c177;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font: 16px/1.6 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background: linear-gradient(180deg, #0c1016, var(--bg));
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px 72px;
|
||||||
|
}
|
||||||
|
h1, h2 { line-height: 1.15; }
|
||||||
|
h1 { margin: 0 0 12px; font-size: 2rem; }
|
||||||
|
h2 { margin: 0 0 14px; font-size: 1.2rem; }
|
||||||
|
p, li { color: var(--text); }
|
||||||
|
.lede { color: var(--muted); max-width: 70ch; }
|
||||||
|
section {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 22px 24px;
|
||||||
|
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
code, pre {
|
||||||
|
font: 13px/1.5 "SFMono-Regular", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(124, 196, 255, 0.12);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 14px 0 0;
|
||||||
|
padding: 14px 16px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: #0d1219;
|
||||||
|
color: #d7e7ff;
|
||||||
|
}
|
||||||
|
ul { margin: 0; padding-left: 1.2rem; }
|
||||||
|
.meta {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
.good { color: var(--good); }
|
||||||
|
.warn { color: var(--warn); }
|
||||||
|
a { color: var(--accent); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="chip">Turn document</span>
|
||||||
|
<span class="chip">2026-05-15</span>
|
||||||
|
<span class="chip">Issue: islandflow-qh7</span>
|
||||||
|
</div>
|
||||||
|
<h1>Dual-runtime deploy workflow</h1>
|
||||||
|
<p class="lede">
|
||||||
|
Updated the root deploy flow so it can target either the existing Docker Compose VPS runtime or a new host-native Bun + systemd runtime, while also adding partial deploy scopes for faster iteration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<p>
|
||||||
|
The deploy helper now supports <code>--runtime docker</code> and <code>--runtime native</code>, keeps Docker as the default, and adds <code>--web-only</code>, <code>--api-only</code>, <code>--services-only</code>, and <code>--no-build</code>. Documentation now clearly separates fast local development from VPS rollout options.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Changes Made</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Refactored <code>scripts/deploy.ts</code> into shared git/publish logic plus runtime-specific precheck, rollout, and verification paths.</li>
|
||||||
|
<li>Removed Docker verification’s dependence on hardcoded container names and switched to <code>docker compose exec</code>.</li>
|
||||||
|
<li>Added native deployment support that assumes Bun plus systemd-managed units on the VPS.</li>
|
||||||
|
<li>Added a new operator guide at <code>deployment/native/README.md</code>.</li>
|
||||||
|
<li>Updated <code>README.md</code> to emphasize the preferred fast local loop: Docker infra only, native Bun services, native web dev.</li>
|
||||||
|
<li>Updated <code>deployment/docker/README.md</code> to document Docker as the default runtime and show new partial rollout examples.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Context</h2>
|
||||||
|
<p>
|
||||||
|
The repo already separated local infra from application processes: the root <code>docker-compose.yml</code> is infra-only, while services and the web app run through Bun scripts. The old deploy helper still assumed every server rollout was Docker-only. This change makes the deploy workflow match the new operating model: fast native iteration locally, Docker still available in production, and a native VPS path available during transition.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Important Implementation Details</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Docker remains the default runtime, so <code>./deploy main</code> still works without extra flags.</li>
|
||||||
|
<li>Native rollouts are invoked with <code>./deploy main --runtime native</code> or <code>./deploy current-branch --runtime native</code>.</li>
|
||||||
|
<li>Partial scopes are mutually exclusive and intentionally simple:</li>
|
||||||
|
</ul>
|
||||||
|
<pre>./deploy main --runtime docker --web-only
|
||||||
|
./deploy main --runtime native --api-only
|
||||||
|
./deploy current-branch --runtime docker --services-only
|
||||||
|
./deploy main --runtime native --web-only --no-build</pre>
|
||||||
|
<ul style="margin-top:14px;">
|
||||||
|
<li>Docker workspace snapshot validation now runs only when a Docker rollout will build images.</li>
|
||||||
|
<li>Native rollouts assume systemd unit names like <code>islandflow-web</code> and <code>islandflow-api</code>, but those names can be overridden with environment variables such as <code>DEPLOY_NATIVE_WEB_UNIT</code>.</li>
|
||||||
|
<li>The native path also allows overriding the systemctl wrapper via <code>DEPLOY_NATIVE_SYSTEMCTL_PREFIX</code>, which is useful for <code>systemctl --user</code> setups.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Validation</h2>
|
||||||
|
<ul>
|
||||||
|
<li class="good">Passed: <code>bun run scripts/deploy.ts --help</code></li>
|
||||||
|
<li class="good">Passed: <code>bun run check:docker-workspace</code></li>
|
||||||
|
<li class="good">Passed: invalid-flag guard for <code>--runtime native --force-recreate</code></li>
|
||||||
|
<li class="good">Passed: conflicting-scope guard for <code>--web-only --api-only</code></li>
|
||||||
|
</ul>
|
||||||
|
<pre>bun run scripts/deploy.ts --help
|
||||||
|
bun run check:docker-workspace
|
||||||
|
bun run scripts/deploy.ts main --runtime native --force-recreate
|
||||||
|
bun run scripts/deploy.ts main --web-only --api-only</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Issues, Limitations, and Mitigations</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span class="warn">Native deploys assume server-side systemd units already exist.</span> Mitigation: added <code>deployment/native/README.md</code> documenting expected unit names and override variables.</li>
|
||||||
|
<li><span class="warn">Rollback is still manual.</span> Mitigation: both Docker and native docs now frame runtime selection as a transition path, with Docker preserved as a fallback.</li>
|
||||||
|
<li><span class="warn">No native service unit files were added in this change.</span> This keeps the scope focused on the deploy workflow itself.</li>
|
||||||
|
<li><span class="warn">Public verification still centers on the hosted app URL.</span> API verification remains local-to-runtime unless <code>DEPLOY_PUBLIC_API_HEALTH_URL</code> is configured.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Follow-up Work</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Implementation tracked in <code>islandflow-qh7</code> is complete for the deploy helper itself.</li>
|
||||||
|
<li>Open follow-up: <code>islandflow-38p</code>, add checked-in native deployment unit templates and rollback helpers.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -6,10 +6,20 @@ import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
type DeployMode = "main" | "current-branch";
|
type DeployMode = "main" | "current-branch";
|
||||||
|
type DeployRuntime = "docker" | "native";
|
||||||
|
type DeployScope = "full" | "web" | "api" | "services";
|
||||||
|
|
||||||
|
type DeployOptions = {
|
||||||
|
mode: DeployMode;
|
||||||
|
runtime: DeployRuntime;
|
||||||
|
scope: DeployScope;
|
||||||
|
forceRecreate: boolean;
|
||||||
|
noBuild: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const REMOTE_HOST = "delta@152.53.80.229";
|
const REMOTE_HOST = "delta@152.53.80.229";
|
||||||
const REMOTE_REPO = "/home/delta/islandflow";
|
const REMOTE_REPO = "/home/delta/islandflow";
|
||||||
const REMOTE_DEPLOYMENT = "/home/delta/islandflow/deployment/docker";
|
const REMOTE_DOCKER_DEPLOYMENT = "/home/delta/islandflow/deployment/docker";
|
||||||
const SSH_KEY = path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519");
|
const SSH_KEY = path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519");
|
||||||
const SSH_OPTIONS = [
|
const SSH_OPTIONS = [
|
||||||
"-i",
|
"-i",
|
||||||
|
|
@ -17,47 +27,83 @@ const SSH_OPTIONS = [
|
||||||
"-o",
|
"-o",
|
||||||
"IdentitiesOnly=yes",
|
"IdentitiesOnly=yes",
|
||||||
"-o",
|
"-o",
|
||||||
"BatchMode=yes",
|
"BatchMode=yes"
|
||||||
];
|
];
|
||||||
const ALLOWED_REMOTE_UNTRACKED = new Set([
|
const ALLOWED_REMOTE_UNTRACKED = new Set([
|
||||||
"deployment/docker/signal-cli-0.14.3-Linux-native.tar.gz",
|
"deployment/docker/signal-cli-0.14.3-Linux-native.tar.gz",
|
||||||
"deployment/npm/",
|
"deployment/npm/"
|
||||||
]);
|
]);
|
||||||
const API_CONTAINER = "islandflow-vps-api-1";
|
|
||||||
const WEB_CONTAINER = "islandflow-vps-web-1";
|
|
||||||
const PUBLIC_APP_URL =
|
const PUBLIC_APP_URL =
|
||||||
process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io";
|
process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io";
|
||||||
const PUBLIC_API_HEALTH_URL =
|
const PUBLIC_API_HEALTH_URL =
|
||||||
process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
|
process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
|
||||||
const LOG_SERVICES = [
|
const NATIVE_SYSTEMCTL_PREFIX =
|
||||||
|
process.env.DEPLOY_NATIVE_SYSTEMCTL_PREFIX?.trim() || "sudo systemctl";
|
||||||
|
const NATIVE_UNITS = {
|
||||||
|
web: process.env.DEPLOY_NATIVE_WEB_UNIT?.trim() || "islandflow-web",
|
||||||
|
api: process.env.DEPLOY_NATIVE_API_UNIT?.trim() || "islandflow-api",
|
||||||
|
compute: process.env.DEPLOY_NATIVE_COMPUTE_UNIT?.trim() || "islandflow-compute",
|
||||||
|
candles: process.env.DEPLOY_NATIVE_CANDLES_UNIT?.trim() || "islandflow-candles",
|
||||||
|
ingestOptions:
|
||||||
|
process.env.DEPLOY_NATIVE_INGEST_OPTIONS_UNIT?.trim() || "islandflow-ingest-options",
|
||||||
|
ingestEquities:
|
||||||
|
process.env.DEPLOY_NATIVE_INGEST_EQUITIES_UNIT?.trim() || "islandflow-ingest-equities"
|
||||||
|
} as const;
|
||||||
|
const DOCKER_CORE_SERVICES = [
|
||||||
"api",
|
"api",
|
||||||
"web",
|
"web",
|
||||||
"compute",
|
"compute",
|
||||||
"candles",
|
"candles",
|
||||||
"ingest-options",
|
"ingest-options",
|
||||||
"ingest-equities",
|
"ingest-equities"
|
||||||
];
|
] as const;
|
||||||
|
const DOCKER_BACKEND_SERVICES = [
|
||||||
|
"api",
|
||||||
|
"compute",
|
||||||
|
"candles",
|
||||||
|
"ingest-options",
|
||||||
|
"ingest-equities"
|
||||||
|
] as const;
|
||||||
|
|
||||||
const scriptPath = fileURLToPath(import.meta.url);
|
const scriptPath = fileURLToPath(import.meta.url);
|
||||||
const repoRoot = path.resolve(path.dirname(scriptPath), "..");
|
const repoRoot = path.resolve(path.dirname(scriptPath), "..");
|
||||||
|
|
||||||
function usage(exitCode = 1): never {
|
function usage(exitCode = 1): never {
|
||||||
console.error(`Usage:
|
console.error(`Usage:
|
||||||
./deploy main [--force-recreate]
|
./deploy main [--runtime docker|native] [--web-only|--api-only|--services-only] [--no-build] [--force-recreate]
|
||||||
./deploy current-branch [--force-recreate]
|
./deploy current-branch [--runtime docker|native] [--web-only|--api-only|--services-only] [--no-build] [--force-recreate]
|
||||||
./deploy current branch [--force-recreate]
|
./deploy current branch [--runtime docker|native] [--web-only|--api-only|--services-only] [--no-build] [--force-recreate]
|
||||||
|
|
||||||
Modes:
|
Modes:
|
||||||
main Deploy origin/main to the live server checkout.
|
main Deploy origin/main to the live server checkout.
|
||||||
current-branch Push the current local branch, switch the server to it, and deploy it.
|
current-branch Push the current local branch, switch the server to it, and deploy it.
|
||||||
|
|
||||||
|
Runtimes:
|
||||||
|
docker Roll out from deployment/docker with Docker Compose (default).
|
||||||
|
native Roll out host-native Bun services managed by systemd.
|
||||||
|
|
||||||
|
Scopes:
|
||||||
|
default Full rollout (web + API + backend services).
|
||||||
|
--web-only Deploy only the Next.js web surface.
|
||||||
|
--api-only Deploy only the API service.
|
||||||
|
--services-only Deploy API + backend services without the web service.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--force-recreate Escalation path for docker compose when a normal refresh is not enough.
|
--runtime <name> Explicit runtime selector (docker or native).
|
||||||
|
--no-build Skip docker image builds or native bun install/web build steps.
|
||||||
|
--force-recreate Docker-only escalation path for docker compose when a normal refresh is not enough.
|
||||||
--help Show this help text.
|
--help Show this help text.
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
DEPLOY_PUBLIC_APP_URL Override the public app URL (default: https://flow.deltaisland.io).
|
DEPLOY_PUBLIC_APP_URL Override the public app URL (default: https://flow.deltaisland.io).
|
||||||
DEPLOY_PUBLIC_API_HEALTH_URL Optional separate public API health URL for two-origin deployments.`);
|
DEPLOY_PUBLIC_API_HEALTH_URL Optional separate public API health URL for two-origin deployments.
|
||||||
|
DEPLOY_NATIVE_SYSTEMCTL_PREFIX Override systemctl invocation for native rollouts (default: sudo systemctl).
|
||||||
|
DEPLOY_NATIVE_WEB_UNIT Override native web systemd unit name.
|
||||||
|
DEPLOY_NATIVE_API_UNIT Override native api systemd unit name.
|
||||||
|
DEPLOY_NATIVE_COMPUTE_UNIT Override native compute systemd unit name.
|
||||||
|
DEPLOY_NATIVE_CANDLES_UNIT Override native candles systemd unit name.
|
||||||
|
DEPLOY_NATIVE_INGEST_OPTIONS_UNIT Override native ingest-options systemd unit name.
|
||||||
|
DEPLOY_NATIVE_INGEST_EQUITIES_UNIT Override native ingest-equities systemd unit name.`);
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,13 +120,13 @@ function formatCommand(command: string, args: string[]): string {
|
||||||
function runChecked(
|
function runChecked(
|
||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
options: SpawnSyncOptions = {},
|
options: SpawnSyncOptions = {}
|
||||||
): void {
|
): void {
|
||||||
console.log(`$ ${formatCommand(command, args)}`);
|
console.log(`$ ${formatCommand(command, args)}`);
|
||||||
const result = spawnSync(command, args, {
|
const result = spawnSync(command, args, {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
...options,
|
...options
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
|
|
@ -91,13 +137,13 @@ function runChecked(
|
||||||
function captureChecked(
|
function captureChecked(
|
||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
options: SpawnSyncOptions = {},
|
options: SpawnSyncOptions = {}
|
||||||
): string {
|
): string {
|
||||||
const result = spawnSync(command, args, {
|
const result = spawnSync(command, args, {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
stdio: ["inherit", "pipe", "pipe"],
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
...options,
|
...options
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
|
|
@ -111,7 +157,7 @@ function captureChecked(
|
||||||
function runRemoteScript(
|
function runRemoteScript(
|
||||||
title: string,
|
title: string,
|
||||||
script: string,
|
script: string,
|
||||||
args: string[] = [],
|
args: string[] = []
|
||||||
): void {
|
): void {
|
||||||
section(title);
|
section(title);
|
||||||
const sshArgs = [...SSH_OPTIONS, REMOTE_HOST, "bash", "-s", "--", ...args];
|
const sshArgs = [...SSH_OPTIONS, REMOTE_HOST, "bash", "-s", "--", ...args];
|
||||||
|
|
@ -120,7 +166,7 @@ function runRemoteScript(
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
input: script,
|
input: script,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
stdio: ["pipe", "inherit", "inherit"],
|
stdio: ["pipe", "inherit", "inherit"]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
|
|
@ -128,28 +174,85 @@ function runRemoteScript(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(rawArgs: string[]): {
|
function parseRuntime(rawArgs: string[]): DeployRuntime {
|
||||||
mode: DeployMode;
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
||||||
forceRecreate: boolean;
|
const arg = rawArgs[index];
|
||||||
} {
|
if (arg === "--runtime") {
|
||||||
|
const value = rawArgs[index + 1];
|
||||||
|
if (value === "docker" || value === "native") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith("--runtime=")) {
|
||||||
|
const value = arg.slice("--runtime=".length);
|
||||||
|
if (value === "docker" || value === "native") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "docker";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScope(rawArgs: string[]): DeployScope {
|
||||||
|
const scopes = [
|
||||||
|
rawArgs.includes("--web-only") ? "web" : null,
|
||||||
|
rawArgs.includes("--api-only") ? "api" : null,
|
||||||
|
rawArgs.includes("--services-only") ? "services" : null
|
||||||
|
].filter((value): value is Exclude<DeployScope, "full"> => value !== null);
|
||||||
|
|
||||||
|
if (scopes.length > 1) {
|
||||||
|
console.error("Choose only one deploy scope flag: --web-only, --api-only, or --services-only.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes[0] ?? "full";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(rawArgs: string[]): DeployOptions {
|
||||||
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
||||||
usage(0);
|
usage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runtime = parseRuntime(rawArgs);
|
||||||
|
const scope = parseScope(rawArgs);
|
||||||
const forceRecreate = rawArgs.includes("--force-recreate");
|
const forceRecreate = rawArgs.includes("--force-recreate");
|
||||||
const positional = rawArgs.filter((arg) => arg !== "--force-recreate");
|
const noBuild = rawArgs.includes("--no-build");
|
||||||
|
const positional = rawArgs.filter(
|
||||||
|
(arg, index) =>
|
||||||
|
arg !== "--force-recreate" &&
|
||||||
|
arg !== "--no-build" &&
|
||||||
|
arg !== "--web-only" &&
|
||||||
|
arg !== "--api-only" &&
|
||||||
|
arg !== "--services-only" &&
|
||||||
|
arg !== "--runtime" &&
|
||||||
|
rawArgs[index - 1] !== "--runtime" &&
|
||||||
|
!arg.startsWith("--runtime=")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (forceRecreate && runtime !== "docker") {
|
||||||
|
console.error("--force-recreate is only supported with --runtime docker.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (positional.length === 1 && positional[0] === "main") {
|
if (positional.length === 1 && positional[0] === "main") {
|
||||||
return { mode: "main", forceRecreate };
|
return { mode: "main", runtime, scope, forceRecreate, noBuild };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(positional.length === 1 && positional[0] === "current-branch") ||
|
(positional.length === 1 && positional[0] === "current-branch") ||
|
||||||
(positional.length === 2 &&
|
(positional.length === 2 && positional[0] === "current" && positional[1] === "branch")
|
||||||
positional[0] === "current" &&
|
|
||||||
positional[1] === "branch")
|
|
||||||
) {
|
) {
|
||||||
return { mode: "current-branch", forceRecreate };
|
return {
|
||||||
|
mode: "current-branch",
|
||||||
|
runtime,
|
||||||
|
scope,
|
||||||
|
forceRecreate,
|
||||||
|
noBuild
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
usage();
|
usage();
|
||||||
|
|
@ -162,28 +265,122 @@ function assertSshKeyExists(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function localWorkspaceSnapshotPrecheck(): void {
|
function shellEscape(value: string): string {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return "''";
|
||||||
|
}
|
||||||
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellPattern(value: string): string {
|
||||||
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeRuntime(runtime: DeployRuntime): string {
|
||||||
|
return runtime === "docker" ? "Docker Compose" : "native systemd/Bun";
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeScope(scope: DeployScope): string {
|
||||||
|
switch (scope) {
|
||||||
|
case "web":
|
||||||
|
return "web only";
|
||||||
|
case "api":
|
||||||
|
return "api only";
|
||||||
|
case "services":
|
||||||
|
return "api + backend services";
|
||||||
|
default:
|
||||||
|
return "full stack";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeIncludesWeb(scope: DeployScope): boolean {
|
||||||
|
return scope === "full" || scope === "web";
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeIncludesApi(scope: DeployScope): boolean {
|
||||||
|
return scope === "full" || scope === "api" || scope === "services";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockerServicesForScope(scope: DeployScope): string[] {
|
||||||
|
switch (scope) {
|
||||||
|
case "web":
|
||||||
|
return ["web"];
|
||||||
|
case "api":
|
||||||
|
return ["api"];
|
||||||
|
case "services":
|
||||||
|
return [...DOCKER_BACKEND_SERVICES];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockerLogServicesForScope(scope: DeployScope): string[] {
|
||||||
|
switch (scope) {
|
||||||
|
case "web":
|
||||||
|
return ["web"];
|
||||||
|
case "api":
|
||||||
|
return ["api"];
|
||||||
|
case "services":
|
||||||
|
return [...DOCKER_BACKEND_SERVICES];
|
||||||
|
default:
|
||||||
|
return [...DOCKER_CORE_SERVICES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeUnitsForScope(scope: DeployScope): string[] {
|
||||||
|
switch (scope) {
|
||||||
|
case "web":
|
||||||
|
return [NATIVE_UNITS.web];
|
||||||
|
case "api":
|
||||||
|
return [NATIVE_UNITS.api];
|
||||||
|
case "services":
|
||||||
|
return [
|
||||||
|
NATIVE_UNITS.api,
|
||||||
|
NATIVE_UNITS.compute,
|
||||||
|
NATIVE_UNITS.candles,
|
||||||
|
NATIVE_UNITS.ingestOptions,
|
||||||
|
NATIVE_UNITS.ingestEquities
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
NATIVE_UNITS.web,
|
||||||
|
NATIVE_UNITS.api,
|
||||||
|
NATIVE_UNITS.compute,
|
||||||
|
NATIVE_UNITS.candles,
|
||||||
|
NATIVE_UNITS.ingestOptions,
|
||||||
|
NATIVE_UNITS.ingestEquities
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function localDockerWorkspaceSnapshotPrecheck(): void {
|
||||||
console.log("$ bun run check:docker-workspace");
|
console.log("$ bun run check:docker-workspace");
|
||||||
const result = spawnSync("bun", ["run", "check:docker-workspace"], {
|
const result = spawnSync("bun", ["run", "check:docker-workspace"], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
stdio: "inherit",
|
stdio: "inherit"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
console.error(
|
console.error(
|
||||||
"Refusing deploy: deployment/docker/workspace-root is out of sync. Run `bun run sync:docker-workspace`, commit updated snapshot files, then retry deploy.",
|
"Refusing docker deploy: deployment/docker/workspace-root is out of sync. Run `bun run sync:docker-workspace`, commit updated snapshot files, then retry deploy."
|
||||||
);
|
);
|
||||||
process.exit(result.status ?? 1);
|
process.exit(result.status ?? 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function localMainPrecheck(): void {
|
function localRuntimePrecheck(runtime: DeployRuntime, noBuild: boolean): void {
|
||||||
|
if (runtime === "docker" && !noBuild) {
|
||||||
|
localDockerWorkspaceSnapshotPrecheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function localMainPrecheck(runtime: DeployRuntime, noBuild: boolean): void {
|
||||||
section("Local Precheck");
|
section("Local Precheck");
|
||||||
runChecked("git", ["fetch", "origin"]);
|
runChecked("git", ["fetch", "origin"]);
|
||||||
runChecked("git", ["status", "--short", "--branch"]);
|
runChecked("git", ["status", "--short", "--branch"]);
|
||||||
runChecked("git", ["rev-parse", "--verify", "HEAD"]);
|
runChecked("git", ["rev-parse", "--verify", "HEAD"]);
|
||||||
runChecked("git", ["rev-parse", "origin/main"]);
|
runChecked("git", ["rev-parse", "origin/main"]);
|
||||||
localWorkspaceSnapshotPrecheck();
|
localRuntimePrecheck(runtime, noBuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentBranchName(): string {
|
function currentBranchName(): string {
|
||||||
|
|
@ -195,7 +392,11 @@ function currentBranchName(): string {
|
||||||
return branch;
|
return branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function localBranchPrecheck(branch: string): void {
|
function localBranchPrecheck(
|
||||||
|
branch: string,
|
||||||
|
runtime: DeployRuntime,
|
||||||
|
noBuild: boolean
|
||||||
|
): void {
|
||||||
section("Local Precheck");
|
section("Local Precheck");
|
||||||
runChecked("git", ["branch", "--show-current"]);
|
runChecked("git", ["branch", "--show-current"]);
|
||||||
runChecked("git", ["status", "--short", "--branch"]);
|
runChecked("git", ["status", "--short", "--branch"]);
|
||||||
|
|
@ -204,12 +405,12 @@ function localBranchPrecheck(branch: string): void {
|
||||||
const porcelain = captureChecked("git", ["status", "--porcelain=v1"]).trim();
|
const porcelain = captureChecked("git", ["status", "--porcelain=v1"]).trim();
|
||||||
if (porcelain) {
|
if (porcelain) {
|
||||||
console.error(
|
console.error(
|
||||||
`Refusing to deploy ${branch} with uncommitted local changes. Commit the intended state first.`,
|
`Refusing to deploy ${branch} with uncommitted local changes. Commit the intended state first.`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
localWorkspaceSnapshotPrecheck();
|
localRuntimePrecheck(runtime, noBuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishCurrentBranch(branch: string): void {
|
function publishCurrentBranch(branch: string): void {
|
||||||
|
|
@ -220,8 +421,8 @@ function publishCurrentBranch(branch: string): void {
|
||||||
{
|
{
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
stdio: ["inherit", "pipe", "pipe"],
|
stdio: ["inherit", "pipe", "pipe"]
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (upstreamResult.status === 0) {
|
if (upstreamResult.status === 0) {
|
||||||
|
|
@ -232,9 +433,9 @@ function publishCurrentBranch(branch: string): void {
|
||||||
runChecked("git", ["push", "-u", "origin", branch]);
|
runChecked("git", ["push", "-u", "origin", branch]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remotePrecheck(): void {
|
function remoteGitPrecheck(): void {
|
||||||
const allowedRemoteUntrackedPattern = Array.from(ALLOWED_REMOTE_UNTRACKED)
|
const allowedRemoteUntrackedPattern = Array.from(ALLOWED_REMOTE_UNTRACKED)
|
||||||
.map((path) => shellPattern(path))
|
.map((value) => shellPattern(value))
|
||||||
.join("|");
|
.join("|");
|
||||||
|
|
||||||
runRemoteScript(
|
runRemoteScript(
|
||||||
|
|
@ -242,7 +443,7 @@ function remotePrecheck(): void {
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "${REMOTE_REPO}"
|
cd ${shellEscape(REMOTE_REPO)}
|
||||||
status="$(git status --porcelain=v1 --branch)"
|
status="$(git status --porcelain=v1 --branch)"
|
||||||
git status --short --branch
|
git status --short --branch
|
||||||
git branch --show-current
|
git branch --show-current
|
||||||
|
|
@ -269,104 +470,268 @@ while IFS= read -r line; do
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done <<< "$status"
|
done <<< "$status"
|
||||||
`,
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteRollout(
|
function remoteRuntimePrecheck(runtime: DeployRuntime, scope: DeployScope): void {
|
||||||
mode: DeployMode,
|
if (runtime === "docker") {
|
||||||
branch: string | null,
|
runRemoteScript(
|
||||||
forceRecreate: boolean,
|
"Remote Runtime Precheck",
|
||||||
): void {
|
`#!/usr/bin/env bash
|
||||||
const composeArgs = forceRecreate
|
set -euo pipefail
|
||||||
? "up -d --build --force-recreate"
|
|
||||||
: "up -d --build";
|
cd ${shellEscape(REMOTE_DOCKER_DEPLOYMENT)}
|
||||||
|
command -v docker >/dev/null 2>&1
|
||||||
|
|
||||||
|
docker compose version >/dev/null
|
||||||
|
`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
|
||||||
|
runRemoteScript(
|
||||||
|
"Remote Runtime Precheck",
|
||||||
|
`#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd ${shellEscape(REMOTE_REPO)}
|
||||||
|
command -v bun >/dev/null 2>&1
|
||||||
|
command -v systemctl >/dev/null 2>&1
|
||||||
|
|
||||||
|
declare -a units=(${units})
|
||||||
|
for unit in "\${units[@]}"; do
|
||||||
|
load_state="$(${NATIVE_SYSTEMCTL_PREFIX} show --property=LoadState --value "$unit" 2>/dev/null || true)"
|
||||||
|
if [[ -z "$load_state" || "$load_state" == "not-found" ]]; then
|
||||||
|
echo "Refusing native rollout: missing systemd unit $unit" >&2
|
||||||
|
echo "See deployment/native/README.md for expected unit names and overrides." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteGitUpdateScript(mode: DeployMode, branch: string | null): string {
|
||||||
|
const escapedBranch = branch ? shellEscape(branch) : null;
|
||||||
const switchCommand =
|
const switchCommand =
|
||||||
mode === "main"
|
mode === "main"
|
||||||
? `git switch main
|
? `git switch main\ngit pull --ff-only origin main`
|
||||||
git pull --ff-only origin main`
|
: `git switch ${escapedBranch} || git switch -c ${escapedBranch} --track origin/${escapedBranch}\ngit pull --ff-only origin ${escapedBranch}`;
|
||||||
: `git switch ${shellEscape(branch!)} || git switch -c ${shellEscape(branch!)} --track origin/${shellEscape(branch!)}
|
|
||||||
git pull --ff-only origin ${shellEscape(branch!)}`;
|
return `cd ${shellEscape(REMOTE_REPO)}\ngit fetch origin\n${switchCommand}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteDockerRollout(
|
||||||
|
mode: DeployMode,
|
||||||
|
branch: string | null,
|
||||||
|
scope: DeployScope,
|
||||||
|
forceRecreate: boolean,
|
||||||
|
noBuild: boolean
|
||||||
|
): void {
|
||||||
|
const services = dockerServicesForScope(scope);
|
||||||
|
const args = ["up", "-d"];
|
||||||
|
if (!noBuild) {
|
||||||
|
args.push("--build");
|
||||||
|
}
|
||||||
|
if (forceRecreate) {
|
||||||
|
args.push("--force-recreate");
|
||||||
|
}
|
||||||
|
const command = `docker compose ${[...args, ...services].join(" ")}`;
|
||||||
|
|
||||||
runRemoteScript(
|
runRemoteScript(
|
||||||
"Remote Rollout",
|
"Remote Rollout",
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "${REMOTE_REPO}"
|
${remoteGitUpdateScript(mode, branch)}
|
||||||
git fetch origin
|
|
||||||
${switchCommand}
|
|
||||||
|
|
||||||
cd "${REMOTE_DEPLOYMENT}"
|
cd ${shellEscape(REMOTE_DOCKER_DEPLOYMENT)}
|
||||||
docker compose ${composeArgs}
|
${command}
|
||||||
`,
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteVerification(): void {
|
function remoteNativeRollout(
|
||||||
|
mode: DeployMode,
|
||||||
|
branch: string | null,
|
||||||
|
scope: DeployScope,
|
||||||
|
noBuild: boolean
|
||||||
|
): void {
|
||||||
|
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
|
||||||
|
const buildSteps: string[] = [];
|
||||||
|
|
||||||
|
if (!noBuild) {
|
||||||
|
buildSteps.push("bun install --frozen-lockfile");
|
||||||
|
if (scopeIncludesWeb(scope)) {
|
||||||
|
buildSteps.push("bun --cwd=apps/web run build");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSteps.push(`${NATIVE_SYSTEMCTL_PREFIX} restart ${nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ")}`);
|
||||||
|
|
||||||
|
runRemoteScript(
|
||||||
|
"Remote Rollout",
|
||||||
|
`#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
${remoteGitUpdateScript(mode, branch)}
|
||||||
|
|
||||||
|
cd ${shellEscape(REMOTE_REPO)}
|
||||||
|
${buildSteps.join("\n")}
|
||||||
|
|
||||||
|
declare -a units=(${units})
|
||||||
|
for unit in "\${units[@]}"; do
|
||||||
|
${NATIVE_SYSTEMCTL_PREFIX} is-active --quiet "$unit"
|
||||||
|
done
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteRollout(
|
||||||
|
mode: DeployMode,
|
||||||
|
runtime: DeployRuntime,
|
||||||
|
branch: string | null,
|
||||||
|
scope: DeployScope,
|
||||||
|
forceRecreate: boolean,
|
||||||
|
noBuild: boolean
|
||||||
|
): void {
|
||||||
|
if (runtime === "docker") {
|
||||||
|
remoteDockerRollout(mode, branch, scope, forceRecreate, noBuild);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteNativeRollout(mode, branch, scope, noBuild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteDockerVerification(scope: DeployScope): void {
|
||||||
|
const psServices = dockerServicesForScope(scope);
|
||||||
|
const logServices = dockerLogServicesForScope(scope);
|
||||||
|
const psCommand =
|
||||||
|
psServices.length > 0
|
||||||
|
? `docker compose ps ${psServices.join(" ")}`
|
||||||
|
: "docker compose ps";
|
||||||
|
const logCommand = `docker compose logs --tail=100 ${logServices.join(" ")}`;
|
||||||
|
const checks: string[] = [];
|
||||||
|
|
||||||
|
if (scopeIncludesApi(scope)) {
|
||||||
|
checks.push(
|
||||||
|
`docker compose exec -T api bun -e 'const r = await fetch("http://127.0.0.1:4000/health"); if (!r.ok) throw new Error("api healthcheck failed: " + r.status); console.log(await r.text())'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopeIncludesWeb(scope)) {
|
||||||
|
checks.push(
|
||||||
|
`docker compose exec -T web bun -e 'const r = await fetch("http://127.0.0.1:3000/"); if (!r.ok) throw new Error("web healthcheck failed: " + r.status); console.log(r.status)'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
runRemoteScript(
|
runRemoteScript(
|
||||||
"Remote Verification",
|
"Remote Verification",
|
||||||
`#!/usr/bin/env bash
|
`#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "${REMOTE_DEPLOYMENT}"
|
cd ${shellEscape(REMOTE_DOCKER_DEPLOYMENT)}
|
||||||
docker compose ps
|
${psCommand}
|
||||||
docker compose logs --tail=100 ${LOG_SERVICES.join(" ")}
|
${logCommand}
|
||||||
docker exec ${API_CONTAINER} bun -e 'const r = await fetch("http://127.0.0.1:4000/health"); console.log(await r.text())'
|
${checks.join("\n")}
|
||||||
docker exec ${WEB_CONTAINER} bun -e 'const r = await fetch("http://127.0.0.1:3000/"); console.log(r.status)'
|
`
|
||||||
`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function publicVerification(): void {
|
function remoteNativeVerification(scope: DeployScope): void {
|
||||||
|
const units = nativeUnitsForScope(scope).map((value) => shellEscape(value)).join(" ");
|
||||||
|
const checks: string[] = [];
|
||||||
|
|
||||||
|
if (scopeIncludesApi(scope)) {
|
||||||
|
checks.push('curl -fksS http://127.0.0.1:4000/health');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopeIncludesWeb(scope)) {
|
||||||
|
checks.push('curl -I -fksS http://127.0.0.1:3000/');
|
||||||
|
}
|
||||||
|
|
||||||
|
runRemoteScript(
|
||||||
|
"Remote Verification",
|
||||||
|
`#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
declare -a units=(${units})
|
||||||
|
for unit in "\${units[@]}"; do
|
||||||
|
${NATIVE_SYSTEMCTL_PREFIX} is-active --quiet "$unit"
|
||||||
|
${NATIVE_SYSTEMCTL_PREFIX} status --no-pager "$unit" || true
|
||||||
|
journalctl -u "$unit" -n 50 --no-pager || true
|
||||||
|
done
|
||||||
|
${checks.join("\n")}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteVerification(runtime: DeployRuntime, scope: DeployScope): void {
|
||||||
|
if (runtime === "docker") {
|
||||||
|
remoteDockerVerification(scope);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteNativeVerification(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicVerification(scope: DeployScope): void {
|
||||||
section("Public Verification");
|
section("Public Verification");
|
||||||
runChecked("curl", ["-I", "-fksS", PUBLIC_APP_URL]);
|
runChecked("curl", ["-I", "-fksS", PUBLIC_APP_URL]);
|
||||||
|
|
||||||
if (PUBLIC_API_HEALTH_URL) {
|
if (scopeIncludesApi(scope) && PUBLIC_API_HEALTH_URL) {
|
||||||
runChecked("curl", ["-fksS", PUBLIC_API_HEALTH_URL]);
|
runChecked("curl", ["-fksS", PUBLIC_API_HEALTH_URL]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scopeIncludesApi(scope)) {
|
||||||
console.log(
|
console.log(
|
||||||
"Skipping separate public API health check; same-origin mode relies on the public app check plus container-local API verification.",
|
"Skipping separate public API health check; same-origin mode relies on the public app check plus runtime-local API verification."
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function shellEscape(value: string): string {
|
|
||||||
if (value.length === 0) {
|
|
||||||
return "''";
|
|
||||||
}
|
}
|
||||||
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shellPattern(value: string): string {
|
|
||||||
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function main(): void {
|
function main(): void {
|
||||||
const { mode, forceRecreate } = parseArgs(process.argv.slice(2));
|
const options = parseArgs(process.argv.slice(2));
|
||||||
assertSshKeyExists();
|
assertSshKeyExists();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
mode === "main"
|
`Deploying ${options.mode === "main" ? "origin/main" : "the current local branch"} ` +
|
||||||
? "Deploying origin/main to the existing Islandflow VPS checkout."
|
`via ${describeRuntime(options.runtime)} (${describeScope(options.scope)}).`
|
||||||
: "Deploying the current local branch to the existing Islandflow VPS checkout.",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mode === "main") {
|
if (options.mode === "main") {
|
||||||
localMainPrecheck();
|
localMainPrecheck(options.runtime, options.noBuild);
|
||||||
remotePrecheck();
|
remoteGitPrecheck();
|
||||||
remoteRollout(mode, null, forceRecreate);
|
remoteRuntimePrecheck(options.runtime, options.scope);
|
||||||
|
remoteRollout(
|
||||||
|
options.mode,
|
||||||
|
options.runtime,
|
||||||
|
null,
|
||||||
|
options.scope,
|
||||||
|
options.forceRecreate,
|
||||||
|
options.noBuild
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const branch = currentBranchName();
|
const branch = currentBranchName();
|
||||||
localBranchPrecheck(branch);
|
localBranchPrecheck(branch, options.runtime, options.noBuild);
|
||||||
publishCurrentBranch(branch);
|
publishCurrentBranch(branch);
|
||||||
remotePrecheck();
|
remoteGitPrecheck();
|
||||||
remoteRollout(mode, branch, forceRecreate);
|
remoteRuntimePrecheck(options.runtime, options.scope);
|
||||||
|
remoteRollout(
|
||||||
|
options.mode,
|
||||||
|
options.runtime,
|
||||||
|
branch,
|
||||||
|
options.scope,
|
||||||
|
options.forceRecreate,
|
||||||
|
options.noBuild
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteVerification();
|
remoteVerification(options.runtime, options.scope);
|
||||||
publicVerification();
|
publicVerification(options.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue