rampway 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +41 -0
  2. package/build/src/cli.d.ts.map +1 -1
  3. package/build/src/cli.js +35 -7
  4. package/build/src/cli.js.map +1 -1
  5. package/build/src/commands/task.d.ts +9 -0
  6. package/build/src/commands/task.d.ts.map +1 -0
  7. package/build/src/commands/task.js +23 -0
  8. package/build/src/commands/task.js.map +1 -0
  9. package/build/src/config/validate-config.d.ts +5 -0
  10. package/build/src/config/validate-config.d.ts.map +1 -1
  11. package/build/src/config/validate-config.js +139 -0
  12. package/build/src/config/validate-config.js.map +1 -1
  13. package/build/src/core/deploy-runner.js +5 -5
  14. package/build/src/core/deploy-runner.js.map +1 -1
  15. package/build/src/core/locks.d.ts +2 -1
  16. package/build/src/core/locks.d.ts.map +1 -1
  17. package/build/src/core/locks.js +28 -9
  18. package/build/src/core/locks.js.map +1 -1
  19. package/build/src/core/operational-task-runner.d.ts +21 -0
  20. package/build/src/core/operational-task-runner.d.ts.map +1 -0
  21. package/build/src/core/operational-task-runner.js +66 -0
  22. package/build/src/core/operational-task-runner.js.map +1 -0
  23. package/build/src/core/redaction.d.ts +31 -3
  24. package/build/src/core/redaction.d.ts.map +1 -1
  25. package/build/src/core/redaction.js +83 -5
  26. package/build/src/core/redaction.js.map +1 -1
  27. package/build/src/core/release-manager.d.ts +11 -0
  28. package/build/src/core/release-manager.d.ts.map +1 -1
  29. package/build/src/core/release-manager.js +22 -0
  30. package/build/src/core/release-manager.js.map +1 -1
  31. package/build/src/core/task-runner.d.ts +4 -2
  32. package/build/src/core/task-runner.d.ts.map +1 -1
  33. package/build/src/core/task-runner.js +34 -8
  34. package/build/src/core/task-runner.js.map +1 -1
  35. package/build/src/transports/local-transport.d.ts +19 -2
  36. package/build/src/transports/local-transport.d.ts.map +1 -1
  37. package/build/src/transports/local-transport.js +97 -9
  38. package/build/src/transports/local-transport.js.map +1 -1
  39. package/build/src/transports/ssh-transport.d.ts +43 -4
  40. package/build/src/transports/ssh-transport.d.ts.map +1 -1
  41. package/build/src/transports/ssh-transport.js +105 -12
  42. package/build/src/transports/ssh-transport.js.map +1 -1
  43. package/build/src/types.d.ts +20 -0
  44. package/build/src/types.d.ts.map +1 -1
  45. package/build/src/velocious/auth.d.ts +17 -0
  46. package/build/src/velocious/auth.d.ts.map +1 -0
  47. package/build/src/velocious/auth.js +30 -0
  48. package/build/src/velocious/auth.js.map +1 -0
  49. package/build/src/velocious/control-plane.d.ts +155 -0
  50. package/build/src/velocious/control-plane.d.ts.map +1 -0
  51. package/build/src/velocious/control-plane.js +275 -0
  52. package/build/src/velocious/control-plane.js.map +1 -0
  53. package/build/src/velocious/controller.d.ts +39 -0
  54. package/build/src/velocious/controller.d.ts.map +1 -0
  55. package/build/src/velocious/controller.js +86 -0
  56. package/build/src/velocious/controller.js.map +1 -0
  57. package/build/src/velocious/executor.d.ts +55 -0
  58. package/build/src/velocious/executor.d.ts.map +1 -0
  59. package/build/src/velocious/executor.js +142 -0
  60. package/build/src/velocious/executor.js.map +1 -0
  61. package/build/src/velocious/index.d.ts +28 -0
  62. package/build/src/velocious/index.d.ts.map +1 -0
  63. package/build/src/velocious/index.js +141 -0
  64. package/build/src/velocious/index.js.map +1 -0
  65. package/build/src/velocious/path-matcher.d.ts +18 -0
  66. package/build/src/velocious/path-matcher.d.ts.map +1 -0
  67. package/build/src/velocious/path-matcher.js +48 -0
  68. package/build/src/velocious/path-matcher.js.map +1 -0
  69. package/build/src/velocious/registry.d.ts +19 -0
  70. package/build/src/velocious/registry.d.ts.map +1 -0
  71. package/build/src/velocious/registry.js +36 -0
  72. package/build/src/velocious/registry.js.map +1 -0
  73. package/build/src/velocious/run-store.d.ts +297 -0
  74. package/build/src/velocious/run-store.d.ts.map +1 -0
  75. package/build/src/velocious/run-store.js +482 -0
  76. package/build/src/velocious/run-store.js.map +1 -0
  77. package/build/src/velocious/sanitize.d.ts +23 -0
  78. package/build/src/velocious/sanitize.d.ts.map +1 -0
  79. package/build/src/velocious/sanitize.js +154 -0
  80. package/build/src/velocious/sanitize.js.map +1 -0
  81. package/docs/config-reference.md +42 -0
  82. package/docs/docker-development-environment.md +103 -0
  83. package/docs/migration-from-capistrano.md +25 -3
  84. package/docs/velocious-deployment-api.md +189 -0
  85. package/package.json +22 -3
@@ -0,0 +1,154 @@
1
+ // @ts-check
2
+ const MAX_STRING_LENGTH = 4000;
3
+ const MAX_DEPTH = 20;
4
+ const MAX_COLLECTION_ENTRIES = 100;
5
+ const MAX_VISITED_VALUES = 1000;
6
+ const MAX_SERIALIZED_BYTES = 64 * 1024;
7
+ const MAX_PREVIEW_LENGTH = 8000;
8
+ const REDACTED = "[redacted]";
9
+ const TRUNCATION_MARKER = "[truncated]";
10
+ const TRUNCATED_VALUE = Symbol("truncated");
11
+ /** @typedef {{visited: number}} SanitizerState */
12
+ /**
13
+ * @param {string} value - Value to redact.
14
+ * @param {string[]} secrets - Exact configured secrets.
15
+ * @returns {string} Redacted and bounded value.
16
+ */
17
+ function redactString(value, secrets) {
18
+ let result = value;
19
+ for (const secret of secrets) {
20
+ if (secret.length > 0)
21
+ result = result.split(secret).join(REDACTED);
22
+ }
23
+ return result.length > MAX_STRING_LENGTH ? `${result.slice(0, MAX_STRING_LENGTH)}…` : result;
24
+ }
25
+ /**
26
+ * Converts an integration value to bounded JSON data while redacting secrets
27
+ * in values and keys. Colliding redacted keys are retained with stable suffixes.
28
+ *
29
+ * @param {unknown} value - Integration value.
30
+ * @param {string[]} secrets - Secrets to remove.
31
+ * @param {number} [depth] - Current depth.
32
+ * @returns {any} JSON-safe value or undefined.
33
+ */
34
+ export function sanitizeAdapterValue(value, secrets, depth = 0) {
35
+ const sanitized = sanitizeValue(value, secrets, depth, { visited: 0 });
36
+ if (sanitized === undefined)
37
+ return undefined;
38
+ if (sanitized === TRUNCATED_VALUE)
39
+ return { reason: "Sanitized output exceeded traversal limits", truncated: true };
40
+ const serialized = JSON.stringify(sanitized);
41
+ if (Buffer.byteLength(serialized) <= MAX_SERIALIZED_BYTES)
42
+ return sanitized;
43
+ return {
44
+ preview: serialized.slice(0, MAX_PREVIEW_LENGTH),
45
+ reason: `Sanitized output exceeded ${MAX_SERIALIZED_BYTES} bytes`,
46
+ truncated: true
47
+ };
48
+ }
49
+ /**
50
+ * @param {unknown} value - Integration value.
51
+ * @param {string[]} secrets - Secrets to remove.
52
+ * @param {number} depth - Current depth.
53
+ * @param {SanitizerState} state - Shared traversal budget.
54
+ * @returns {any} JSON-safe value, undefined, or the truncation sentinel.
55
+ */
56
+ function sanitizeValue(value, secrets, depth, state) {
57
+ if (state.visited >= MAX_VISITED_VALUES || depth > MAX_DEPTH)
58
+ return TRUNCATED_VALUE;
59
+ state.visited++;
60
+ if (value === null)
61
+ return null;
62
+ if (typeof value === "string")
63
+ return redactString(value, secrets);
64
+ if (typeof value === "number")
65
+ return Number.isFinite(value) ? value : null;
66
+ if (typeof value === "boolean")
67
+ return value;
68
+ if (Array.isArray(value)) {
69
+ const result = [];
70
+ let truncated = value.length > MAX_COLLECTION_ENTRIES;
71
+ const contentLimit = truncated ? MAX_COLLECTION_ENTRIES - 1 : value.length;
72
+ for (let index = 0; index < contentLimit; index++) {
73
+ const sanitized = sanitizeValue(value[index], secrets, depth + 1, state);
74
+ if (sanitized === TRUNCATED_VALUE) {
75
+ truncated = true;
76
+ break;
77
+ }
78
+ if (sanitized !== undefined)
79
+ result.push(sanitized);
80
+ }
81
+ if (truncated)
82
+ result.push(TRUNCATION_MARKER);
83
+ return result;
84
+ }
85
+ if (typeof value !== "object")
86
+ return undefined;
87
+ const prototype = Object.getPrototypeOf(value);
88
+ if (prototype !== Object.prototype && prototype !== null)
89
+ return undefined;
90
+ /** @type {Record<string, any>} */
91
+ const result = {};
92
+ const keys = [];
93
+ let truncated = false;
94
+ for (const key in value) {
95
+ if (!Object.hasOwn(value, key))
96
+ continue;
97
+ keys.push(key);
98
+ if (keys.length > MAX_COLLECTION_ENTRIES) {
99
+ truncated = true;
100
+ keys.length = MAX_COLLECTION_ENTRIES - 1;
101
+ break;
102
+ }
103
+ }
104
+ for (const key of keys) {
105
+ const sanitized = sanitizeValue(/** @type {Record<string, unknown>} */ (value)[key], secrets, depth + 1, state);
106
+ if (sanitized === TRUNCATED_VALUE) {
107
+ truncated = true;
108
+ break;
109
+ }
110
+ if (sanitized === undefined)
111
+ continue;
112
+ defineAvailableProperty(result, redactString(key, secrets), sanitized);
113
+ }
114
+ if (truncated)
115
+ defineAvailableProperty(result, TRUNCATION_MARKER, true);
116
+ return result;
117
+ }
118
+ /**
119
+ * Defines a property without dropping a colliding diagnostic key.
120
+ *
121
+ * @param {Record<string, any>} target - Sanitized object.
122
+ * @param {string} key - Desired key.
123
+ * @param {unknown} value - Sanitized value.
124
+ * @returns {void}
125
+ */
126
+ function defineAvailableProperty(target, key, value) {
127
+ let availableKey = key;
128
+ let collision = 2;
129
+ while (Object.hasOwn(target, availableKey)) {
130
+ availableKey = `${key}#${collision}`;
131
+ collision++;
132
+ }
133
+ Object.defineProperty(target, availableKey, {
134
+ configurable: true,
135
+ enumerable: true,
136
+ value,
137
+ writable: true
138
+ });
139
+ }
140
+ /**
141
+ * @param {unknown} error - Deployment failure.
142
+ * @param {string[]} secrets - Secrets to remove.
143
+ * @returns {{message: string, recovery: any}} Safe failure payload.
144
+ */
145
+ export function sanitizeErrorPayload(error, secrets) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ const recovery = error instanceof Error ? /** @type {Error & {recovery?: unknown}} */ (error).recovery : undefined;
148
+ const payload = sanitizeAdapterValue({ message, recovery }, secrets);
149
+ if (payload && typeof payload === "object" && typeof payload.message === "string") {
150
+ return { message: payload.message, recovery: payload.recovery };
151
+ }
152
+ return { message: "Deployment failure diagnostics were truncated", recovery: payload };
153
+ }
154
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../../src/velocious/sanitize.js"],"names":[],"mappings":"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"}
@@ -38,6 +38,7 @@ Each stage under `stages` supports:
38
38
  | `linkedFiles` | array | no | `[]` | Files symlinked from `shared/` into each release. See Linked files below. |
39
39
  | `linkedDirs` | array | no | `[]` | Directories symlinked from `shared/` into each release. |
40
40
  | `tasks` | object | no | `{}` | Named task groups executed in order during deploy. See Tasks below. |
41
+ | `operationalTasks` | object | no | `{}` | Explicit standalone tasks that run against the active release. See Operational tasks below. |
41
42
  | `hooks` | object | no | `{}` | Lifecycle-phase hooks executed at specific points in the deploy pipeline. See Hooks below. |
42
43
  | `runtime` | object | no | `{type: "none"}` | Runtime adapter config. See Runtime below. |
43
44
  | `healthChecks` | array | no | `[]` | Checks run after tasks and before publish. See Health checks below. |
@@ -198,6 +199,47 @@ rampway production tasks --json
198
199
 
199
200
  Each entry shows its command plus any `cwd`, `env`, `optional`, and `onlyIfChanged` metadata. Secret-like env values and `KEY=value` assignments in commands are redacted in both the human and `--json` output.
200
201
 
202
+ ## Operational tasks
203
+
204
+ `operationalTasks` is a strict namespace for reusable standalone operations. It is separate from deploy lifecycle `tasks`: an operational task runs only when its exact configured name is invoked.
205
+
206
+ ```js
207
+ operationalTasks: {
208
+ "cache:warm": [
209
+ {
210
+ command: "npm run cache:warm",
211
+ cwd: "backend",
212
+ env: {NODE_ENV: "production"},
213
+ forwardEnv: ["CACHE_WARM_CREDENTIAL"]
214
+ }
215
+ ]
216
+ }
217
+ ```
218
+
219
+ Each value is a command string, one entry object, or a non-empty ordered array of strings and entry objects. Entry objects accept only:
220
+
221
+ - `command` — required non-empty shell command from trusted configuration.
222
+ - `cwd` — optional non-empty release-relative directory; absolute and escaping paths are rejected.
223
+ - `env` — optional string-valued environment literals with safe shell names. Secret-like keys are rejected; use `forwardEnv`.
224
+ - `forwardEnv` — optional unique allowlist of safe environment names. Every named value must exist in Rampway's invocation environment and is always treated as sensitive.
225
+
226
+ Task names are 1–128 characters and match `^[A-Za-z0-9][A-Za-z0-9:_-]*$`. Selection is an exact name lookup; command text and arbitrary trailing arguments cannot come from the CLI.
227
+
228
+ ```bash
229
+ CACHE_WARM_CREDENTIAL=... rampway production task cache:warm
230
+ CACHE_WARM_CREDENTIAL=... rampway production task cache:warm --json
231
+ ```
232
+
233
+ Rampway resolves all forwarded values before transport work, acquires the same exclusive lock used by deploy and rollback, then strictly resolves `current` to one existing direct child of `releases/`. Every entry is required and runs in order against that immutable release path. Missing configuration, forwarded values, lock ownership, active release, command failures, and transport failures all fail closed. There is no task `--force`.
234
+
235
+ For SSH, forwarded values are sent to a fixed remote shell through stdin rather than SSH argv. Known values are redacted from streamed output, returned output, and errors even when echoed without their variable name or split across chunks. Human output identifies the application, stage, task, and release. JSON mode emits only:
236
+
237
+ ```json
238
+ {"application":"demo","stage":"production","task":"cache:warm","releaseId":"20260728120000","status":"passed"}
239
+ ```
240
+
241
+ Standalone task runs are not appended to the deployment report. The plural `rampway <stage> tasks` command remains a read-only inventory of deploy task groups.
242
+
201
243
  ## Hooks
202
244
 
203
245
  Hooks are lifecycle-phase callbacks that run commands at specific points during the deploy pipeline, outside the fixed task-group slot. Each hook phase accepts an array of task entries (strings or objects, same format as tasks).
@@ -0,0 +1,103 @@
1
+ # Docker development environment
2
+
3
+ Rampway has one canonical root development image and one persistent Compose service. The image is digest-pinned Ubuntu 26.04 LTS with exact NodeSource Node 24, a checksummed signing key, the universal coding/debugging baseline, and the current OpenCode, Codex, Claude Code, and official Kimi Code CLIs. The build is source-independent: `.dockerignore` closes the build context to the Dockerfile metadata, the image copies no repository content, and project dependencies are installed only inside the running service.
4
+
5
+ ## First use
6
+
7
+ Prerequisites are Docker with Compose v2 and a dedicated UID/GID-1000 development home containing this checkout at `$DEV_HOME_PATH/rampway`.
8
+
9
+ Copy the portable template and confirm its required read-only credential sources:
10
+
11
+ ```bash
12
+ cp .env.example .env
13
+ test -d "${GH_CONFIG_SOURCE_PATH:-$HOME/.config/gh}"
14
+ test -f "${CODEX_AUTH_SOURCE_PATH:-$HOME/.codex/auth.json}"
15
+ ```
16
+
17
+ Set `DEV_HOME_PATH` in `.env` to a dedicated home path containing this checkout at `$DEV_HOME_PATH/rampway`. Do not use an unrelated general host home or recursively change its ownership in the container. The environment template defaults to `/home/dev`; adjust it for any non-default host layout.
18
+
19
+ Run the preparation helper to create the required mount-target parent directories with correct permissions, then start the persistent service and install locked dependencies:
20
+
21
+ ```bash
22
+ scripts/prepare-dev-home.sh
23
+ docker compose up --build --detach dev
24
+ scripts/docker-run.sh npm ci
25
+ ```
26
+
27
+ `scripts/docker-run.sh` uses `docker compose exec -T dev` and preserves every command argument exactly. The service must already be running. Dependencies, caches, the checkout, and ordinary tool state persist naturally under the complete `/home/dev` bind.
28
+
29
+ ## Isolation and mounts
30
+
31
+ Compose declares the default project name `rampway`. Override it with `COMPOSE_PROJECT_NAME` (or `-p`) and use a distinct `DEV_HOME_PATH` for each concurrent instance. Run `scripts/prepare-dev-home.sh` to create required mount-target parent directories before the first start:
32
+
33
+ ```bash
34
+ COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
35
+ scripts/prepare-dev-home.sh
36
+ COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
37
+ docker compose up --build --detach dev
38
+ ```
39
+
40
+ The `dev` service has exactly these mounts:
41
+
42
+ - `${DEV_HOME_PATH:?Set DEV_HOME_PATH in .env}` at `/home/dev`;
43
+ - `${GH_CONFIG_SOURCE_PATH:?Set GH_CONFIG_SOURCE_PATH in .env}` at `/home/dev/.config/gh`, read-only;
44
+ - `${CODEX_AUTH_SOURCE_PATH:?Set CODEX_AUTH_SOURCE_PATH in .env}` at `/home/dev/.codex/auth.json`, read-only.
45
+
46
+ The GitHub config directory is selected through `GH_CONFIG_DIR`. Only the narrow Codex auth file is authorized; SSH keys, Docker sockets, broad host auth mounts, and separate Claude/Kimi/OpenCode secret paths are not. The tracked topology also forbids source split mounts, named source/session volumes, fixed names/ports, privileged mode, sidecars, and container-side ownership fixups.
47
+
48
+ Provider CLI installation is not provider authorization. Threadwire remains parent-owned and is resolved outside this service through bare unversioned `npx threadwire`; Rampway installs no global or project Threadwire package and tracks no provider workspace profile.
49
+
50
+ ## Static validation
51
+
52
+ The dependency-free verifier checks the image pin, NodeSource verification, provider specs, complete apt baseline, non-root identity, exact mounts, fail-closed interpolation, closed build context, forbidden topology, ignored `.env`, and helper contract. Its focused tests also inject representative regressions:
53
+
54
+ ```bash
55
+ npm run verify:docker-dev-environment
56
+ npm run test:docker-dev-environment
57
+ ```
58
+
59
+ ## Docker-capable coordinator validation
60
+
61
+ The bootstrap service has no Docker access. A coordinator must render Compose and build the image cleanly:
62
+
63
+ ```bash
64
+ export COMPOSE_PROJECT_NAME=rampway-canonical-validation
65
+ docker compose config --quiet
66
+ docker compose config
67
+ docker compose build --pull --no-cache dev
68
+ docker compose up --detach dev
69
+ docker compose exec -T dev sh -lc 'test "$HOME" = /home/dev && test "$PWD" = /home/dev/rampway && test "$(id -u)" = 1000 && test "$(id -g)" = 1000'
70
+ docker compose exec -T dev npm ci
71
+ docker compose exec -T dev npm run verify:docker-dev-environment
72
+ ```
73
+
74
+ To prove two-instance isolation, prepare two UID/GID-1000 development homes with Rampway at `<home>/rampway`, start both, and compare their container IDs:
75
+
76
+ ```bash
77
+ COMPOSE_PROJECT_NAME=rampway-isolation-a DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-a docker compose up --build --detach dev
78
+ COMPOSE_PROJECT_NAME=rampway-isolation-b DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-b docker compose up --build --detach dev
79
+ test "$(COMPOSE_PROJECT_NAME=rampway-isolation-a docker compose ps -q dev)" != "$(COMPOSE_PROJECT_NAME=rampway-isolation-b docker compose ps -q dev)"
80
+ ```
81
+
82
+ For fresh cutover, provision a new dedicated home with Rampway already checked out at `<home>/rampway`, then perform a clean image build and dependency install:
83
+
84
+ ```bash
85
+ export RAMPWAY_COMPOSE_PROJECT=rampway-cutover-fresh
86
+ export RAMPWAY_DEV_HOME=/srv/rampway-dev-homes/cutover-fresh
87
+ test -d "$RAMPWAY_DEV_HOME/rampway/.git"
88
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose build --pull --no-cache dev
89
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
90
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T dev npm ci
91
+ ```
92
+
93
+ For exact resume, reuse the retained home and project name without reinstalling or relocating state, then prove the same checkout revision is visible:
94
+
95
+ ```bash
96
+ test -n "$RAMPWAY_COMPOSE_PROJECT"
97
+ test -d "$RAMPWAY_DEV_HOME/rampway/.git"
98
+ export RAMPWAY_EXPECTED_HEAD="$(git -C "$RAMPWAY_DEV_HOME/rampway" rev-parse HEAD)"
99
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
100
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T -e RAMPWAY_EXPECTED_HEAD="$RAMPWAY_EXPECTED_HEAD" dev sh -lc 'test "$PWD" = /home/dev/rampway && test "$(git rev-parse HEAD)" = "$RAMPWAY_EXPECTED_HEAD"'
101
+ ```
102
+
103
+ The obsolete `/workspace` and named-volume state is not imported. Verify the new service before stopping the old project; removal of old named volumes is a separate destructive coordinator decision.
@@ -17,6 +17,7 @@ Rampway is a JS-native alternative to Capistrano for Node projects. This guide m
17
17
  | `SSHKit` | `transport: {type: "ssh"}` |
18
18
  | `after/before` hooks | `tasks` run in config order, or use lifecycle `hooks` for phase-specific callbacks |
19
19
  | `deploy:starting` … `deploy:finished` | task groups + runtime deploy |
20
+ | Explicit custom task against `current` | `operationalTasks` + `rampway production task <exact-name>` |
20
21
  | `cap production deploy` | `rampway production deploy` |
21
22
  | `cap -T` / `cap production -T` | `rampway production tasks` |
22
23
  | `cap production deploy:rollback` | `rampway production rollback` |
@@ -106,7 +107,27 @@ tasks: {
106
107
  }
107
108
  ```
108
109
 
109
- ### 5. Convert process management
110
+ ### 5. Convert standalone operational tasks
111
+
112
+ Keep operations that were invoked explicitly in Capistrano out of deploy task groups. Declare each one under the strict `operationalTasks` namespace and invoke its exact configured name:
113
+
114
+ ```js
115
+ operationalTasks: {
116
+ "cache:warm": {
117
+ command: "npm run cache:warm",
118
+ cwd: "backend",
119
+ forwardEnv: ["CACHE_WARM_CREDENTIAL"]
120
+ }
121
+ }
122
+ ```
123
+
124
+ ```bash
125
+ CACHE_WARM_CREDENTIAL=... rampway production task cache:warm
126
+ ```
127
+
128
+ Rampway owns the configured transport, shared deploy lock, active-release resolution, and secret-value redaction. Replace the former caller's direct SSH with the Rampway invocation; do not retain an SSH fallback. Operational tasks are explicit and never run as part of every deploy.
129
+
130
+ ### 6. Convert process management
110
131
 
111
132
  Capistrano relies on SSH-based process management (puma, sidekiq, etc.) or custom tasks:
112
133
 
@@ -136,7 +157,7 @@ Rollbridge handles zero-downtime restarts, health checks, and log access. Rampwa
136
157
 
137
158
  For simple setups without a process manager, use `runtime: {type: "none"}` and handle process restarts yourself via task commands.
138
159
 
139
- ### 6. Run the first deploy
160
+ ### 7. Run the first deploy
140
161
 
141
162
  ```bash
142
163
  # Validate
@@ -149,7 +170,7 @@ rampway production plan
149
170
  rampway production deploy
150
171
  ```
151
172
 
152
- ### 7. Verify and cleanup
173
+ ### 8. Verify and cleanup
153
174
 
154
175
  ```bash
155
176
  rampway production status
@@ -162,6 +183,7 @@ After verifying the first deploy works, remove your old Capistrano config files.
162
183
 
163
184
  - **No Ruby dependency.** Rampway runs on Node.js, matching your JS/TS stack.
164
185
  - **Task groups replace hooks.** Order task groups instead of registering `after`/`before` callbacks. For Capistrano-style lifecycle callbacks, use `hooks` with named phases like `before_publish` and `after_cleanup`.
186
+ - **Operational tasks replace explicitly invoked remote tasks.** Configure an exact name under `operationalTasks`; Rampway executes it against the locked active release and securely forwards allowlisted caller environment values.
165
187
  - **Runtime adapters replace process management tasks.** Rollbridge or a custom adapter handles zero-downtime handoff.
166
188
  - **ESM config.** `rampway.config.mjs` uses ES module syntax, not Ruby DSL.
167
189
  - **Health checks are built in.** Add `healthChecks` to verify deploy readiness before publishing.
@@ -0,0 +1,189 @@
1
+ # Velocious callable deployment API
2
+
3
+ `rampway/velocious` is Rampway's authenticated deployment control plane for a
4
+ Velocious application. Rampway owns every deployment-specific decision and
5
+ calls its normal deploy lifecycle; Velocious supplies only route mounting,
6
+ request/connection scopes, database persistence, background execution, and
7
+ framework error events.
8
+
9
+ The API accepts no command, path, repository, branch, environment value, log
10
+ request, or arbitrary Git ref. A caller can select only a configured
11
+ project/stage pair, an immutable lowercase 40-character commit SHA reachable
12
+ from that target's approved release branch, and an idempotency key.
13
+ The idempotency key must be a non-secret client-generated value of at most 255
14
+ characters and must not contain any configured bearer token; matching is
15
+ case-sensitive, and such requests are rejected with `422` before run lookup or
16
+ persistence.
17
+
18
+ ## Install and mount
19
+
20
+ Install Velocious in the application that mounts this optional integration.
21
+ Rampway declares it as an optional peer so applications that only use the CLI
22
+ do not acquire a web-framework dependency.
23
+
24
+ ```sh
25
+ npm install rampway velocious
26
+ ```
27
+
28
+ In the application's Velocious routes file:
29
+
30
+ ```js
31
+ import Routes from "velocious/build/src/routes/index.js"
32
+ import RampwayDeploymentApi from "rampway/velocious"
33
+ import deploymentSecrets from "../config/secrets/deployments.js"
34
+
35
+ const routes = new Routes()
36
+
37
+ routes.draw((route) => {
38
+ route.mount(RampwayDeploymentApi, {
39
+ at: "/rampway/deployments",
40
+ accessTokens: deploymentSecrets.rampwayAccessTokens,
41
+ projects: {
42
+ "my-app": {
43
+ stages: {
44
+ production: {
45
+ configPath: "/srv/my-app/control/rampway.config.mjs",
46
+ releaseBranch: "main"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ })
52
+ })
53
+
54
+ export default {routes}
55
+ ```
56
+
57
+ `accessTokens` and every `configPath` are explicit backend configuration.
58
+ Provision the secrets module outside source control using the application's
59
+ normal secret mechanism. There is deliberately no environment-variable
60
+ fallback. `configPath` must be absolute, and its selected Rampway stage must
61
+ use `remote-git`; the deployment target must have Git access to the stage's
62
+ configured repository. Every `releaseBranch` is checked against Git heads-ref
63
+ rules during mounting, so invalid names such as a trailing dot or `.lock`
64
+ component fail at startup rather than during deployment.
65
+
66
+ Mount options:
67
+
68
+ | Option | Required | Meaning |
69
+ | --- | --- | --- |
70
+ | `at` | yes | HTTP mount prefix. |
71
+ | `accessTokens` | yes | One or more non-empty backend-held bearer tokens. |
72
+ | `projects` | yes | Explicit project/stage allowlist with absolute Rampway config paths and approved release branches. |
73
+ | `databaseIdentifier` | no | Velocious database used for runs/audits; default `default`. |
74
+ | `staleRunTimeoutMs` | no | Ownership lease; default `60000`, minimum `4`. |
75
+
76
+ ## Client calls
77
+
78
+ Create a run from a client whose token is also loaded from explicit backend
79
+ secrets (not an environment fallback or command argument):
80
+
81
+ ```js
82
+ import deploymentClientSecrets from "./config/secrets/deployment-client.js"
83
+
84
+ const baseUrl = "https://app.example/rampway/deployments"
85
+ const headers = {
86
+ Authorization: `Bearer ${deploymentClientSecrets.rampwayAccessToken}`,
87
+ "Content-Type": "application/json"
88
+ }
89
+ const createResponse = await fetch(`${baseUrl}/runs`, {
90
+ method: "POST",
91
+ headers,
92
+ body: JSON.stringify({
93
+ project: "my-app",
94
+ stage: "production",
95
+ revision: "0123456789abcdef0123456789abcdef01234567",
96
+ idempotencyKey: "release-2026-08-03-1"
97
+ })
98
+ })
99
+ const created = await createResponse.json()
100
+ ```
101
+
102
+ Read the durable result:
103
+
104
+ ```js
105
+ const runResponse = await fetch(`${baseUrl}/runs/${encodeURIComponent(created.run.id)}`, {headers})
106
+ const {run, current} = await runResponse.json()
107
+ ```
108
+
109
+ Authentication is accepted only from the `Authorization` header. Never place
110
+ the token in a URL or command argument that is logged; clients should obtain it
111
+ from their own secret store and configure the HTTP library's header directly.
112
+ Rampway never includes tokens in response data, run state, audit payloads, Git
113
+ commands, deploy commands, or deployment reports. Adapter values and object
114
+ keys are JSON-normalized and recursively redacted before persistence. Sanitized
115
+ structures are limited to 100 entries per array/object, 1,000 visited values,
116
+ 20 levels, 4,000 characters per string, and 64 KiB total serialized output;
117
+ explicit truncation markers and a bounded redacted preview retain diagnostic
118
+ context.
119
+
120
+ Create responses are bounded:
121
+
122
+ - `202`: created; execution continues in a fresh Velocious connection context.
123
+ - `200`: matching idempotent replay; the original run is returned.
124
+ - `401`: missing or invalid bearer token.
125
+ - `404`: project/stage is not allowlisted.
126
+ - `409`: key conflict, an active deployment, or operator reconciliation is required.
127
+ - `422`: invalid request parameters.
128
+
129
+ Run status is one of `pending`, `running`, `succeeded`, `failed`, `interrupted`,
130
+ or `reconciliation_required`. Failed runs include sanitized recovery state.
131
+ An immutable revision that is not reachable from the approved branch is
132
+ recorded as a sanitized failed run rather than discarded before admission.
133
+ Readback also reports the current/previous Rampway release and active revision.
134
+ If live status cannot be read, the response still returns the durable run with
135
+ `live_status: null` and a sanitized `live_status_error` annotation; the outage
136
+ is also sent to Velocious framework error reporting.
137
+
138
+ ## Execution and recovery guarantees
139
+
140
+ Rampway serializes durable admission before validating reachability on the
141
+ deployment target, so an idempotent retry storm performs the Git fetch only for
142
+ the created run. It then invokes the same `deploy()` used by the CLI: deploy
143
+ lock, immutable remote-Git release, linked state, build/migration tasks, runtime
144
+ handoff, health checks, `current` link, cleanup, and report persistence. It does
145
+ not implement an alternate deployment engine or arbitrary command bridge. If
146
+ failure occurs after the requested revision becomes active, the executor calls
147
+ Rampway's existing rollback lifecycle with the captured prior current release
148
+ as its explicit target and records whether that previous live state was
149
+ restored.
150
+
151
+ Runs and audits are stored in `rampway_deployment_runs` and
152
+ `rampway_deployment_api_audit_events`, scoped by a stable hash of the mount.
153
+ Target and idempotency advisory locks prevent duplicate concurrent admission,
154
+ including one key racing across targets. Pending/running/terminal transitions
155
+ are fenced by mount, run, owner, status, and (for stale reconciliation) the
156
+ observed heartbeat. A renewed live owner therefore cannot be reclaimed, and an
157
+ expired owner cannot overwrite a reconciled run.
158
+
159
+ Pending work whose lease expires becomes `interrupted` and no longer blocks a
160
+ new run. A stale running run, or a known successful activation whose success
161
+ record could not be persisted, becomes `reconciliation_required` and continues
162
+ to block duplicates until an operator verifies live state. Audit persistence
163
+ failure is reported through both Velocious `framework-error` and `all-error`
164
+ events and never strands deployment execution.
165
+ Keyed retries and run-status polling evaluate the lease before returning, so a
166
+ dead owner's stale `pending` or `running` state is reconciled without requiring
167
+ a different-key deployment attempt.
168
+
169
+ ## Token rotation and revocation
170
+
171
+ Use overlapping tokens for zero-downtime rotation:
172
+
173
+ 1. Add a new random token to the backend secrets list and restart/deploy every
174
+ web worker.
175
+ 2. Move every authorized client to the new token.
176
+ 3. Remove the old token from backend secrets and restart/deploy every worker
177
+ again.
178
+ 4. Confirm the old token receives `401` on every instance.
179
+
180
+ For emergency revocation, remove the compromised token and restart all workers
181
+ immediately. Revocation affects new HTTP requests; an already authenticated,
182
+ durably admitted run continues under its owner lease. Inspect the run and audit
183
+ records, reconcile any ambiguous running result, and rotate any repository or
184
+ host credentials independently if they may also be compromised.
185
+
186
+ Do not print tokens while diagnosing authentication. Record only token version
187
+ or fingerprint metadata in an external secret inventory, never the token
188
+ itself. Rampway framework-error payloads deliberately omit the HTTP request so
189
+ listeners cannot accidentally serialize its Authorization header.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampway",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "JS-native Capistrano-style release-directory deployments with Rollbridge integration.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -54,6 +54,14 @@
54
54
  "types": "./build/src/test-helpers.d.ts",
55
55
  "default": "./build/src/test-helpers.js"
56
56
  },
57
+ "./velocious": {
58
+ "types": "./build/src/velocious/index.d.ts",
59
+ "default": "./build/src/velocious/index.js"
60
+ },
61
+ "./velocious.js": {
62
+ "types": "./build/src/velocious/index.d.ts",
63
+ "default": "./build/src/velocious/index.js"
64
+ },
57
65
  "./package.json": "./package.json"
58
66
  },
59
67
  "imports": {
@@ -64,8 +72,10 @@
64
72
  },
65
73
  "scripts": {
66
74
  "test": "npm run build && node --test tests/**/*.test.js",
67
- "lint": "eslint src/ tests/",
75
+ "lint": "eslint src/ tests/ scripts/",
68
76
  "typecheck": "tsc --noEmit",
77
+ "verify:docker-dev-environment": "node scripts/verify-docker-dev-environment.js",
78
+ "test:docker-dev-environment": "node --test tests/docker-dev-environment.test.js",
69
79
  "clean": "node scripts/clean.js",
70
80
  "compile": "tsc -b",
71
81
  "postcompile": "node scripts/ensure-cli-executable.js",
@@ -84,7 +94,16 @@
84
94
  "devDependencies": {
85
95
  "eslint": "^9.39.2",
86
96
  "eslint-plugin-jsdoc": "^61.5.0",
87
- "typescript": "^5.9.3"
97
+ "typescript": "^5.9.3",
98
+ "velocious": "^1.0.574"
99
+ },
100
+ "peerDependencies": {
101
+ "velocious": "^1.0.574"
102
+ },
103
+ "peerDependenciesMeta": {
104
+ "velocious": {
105
+ "optional": true
106
+ }
88
107
  },
89
108
  "files": [
90
109
  "build",