@delegance/claude-autopilot 5.5.2 → 6.2.2
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.
- package/CHANGELOG.md +935 -6
- package/README.md +55 -0
- package/dist/src/adapters/council/openai.js +12 -6
- package/dist/src/adapters/deploy/_http.d.ts +43 -0
- package/dist/src/adapters/deploy/_http.js +99 -0
- package/dist/src/adapters/deploy/fly.d.ts +206 -0
- package/dist/src/adapters/deploy/fly.js +696 -0
- package/dist/src/adapters/deploy/index.d.ts +2 -0
- package/dist/src/adapters/deploy/index.js +33 -0
- package/dist/src/adapters/deploy/render.d.ts +181 -0
- package/dist/src/adapters/deploy/render.js +550 -0
- package/dist/src/adapters/deploy/types.d.ts +67 -3
- package/dist/src/adapters/deploy/vercel.d.ts +17 -1
- package/dist/src/adapters/deploy/vercel.js +29 -49
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/codex.js +10 -7
- package/dist/src/cli/autopilot.d.ts +71 -0
- package/dist/src/cli/autopilot.js +735 -0
- package/dist/src/cli/brainstorm.d.ts +23 -0
- package/dist/src/cli/brainstorm.js +131 -0
- package/dist/src/cli/costs.d.ts +15 -1
- package/dist/src/cli/costs.js +99 -10
- package/dist/src/cli/deploy.d.ts +3 -3
- package/dist/src/cli/deploy.js +34 -9
- package/dist/src/cli/fix.d.ts +18 -0
- package/dist/src/cli/fix.js +105 -11
- package/dist/src/cli/help-text.d.ts +52 -0
- package/dist/src/cli/help-text.js +400 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.js +719 -245
- package/dist/src/cli/json-envelope.d.ts +187 -0
- package/dist/src/cli/json-envelope.js +270 -0
- package/dist/src/cli/json-mode.d.ts +33 -0
- package/dist/src/cli/json-mode.js +201 -0
- package/dist/src/cli/migrate.d.ts +111 -0
- package/dist/src/cli/migrate.js +305 -0
- package/dist/src/cli/plan.d.ts +81 -0
- package/dist/src/cli/plan.js +149 -0
- package/dist/src/cli/pr.d.ts +106 -0
- package/dist/src/cli/pr.js +191 -19
- package/dist/src/cli/preflight.js +26 -0
- package/dist/src/cli/review.d.ts +27 -0
- package/dist/src/cli/review.js +126 -0
- package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
- package/dist/src/cli/runs-watch-renderer.js +275 -0
- package/dist/src/cli/runs-watch.d.ts +41 -0
- package/dist/src/cli/runs-watch.js +395 -0
- package/dist/src/cli/runs.d.ts +122 -0
- package/dist/src/cli/runs.js +902 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/spec.d.ts +66 -0
- package/dist/src/cli/spec.js +132 -0
- package/dist/src/cli/validate.d.ts +29 -0
- package/dist/src/cli/validate.js +131 -0
- package/dist/src/core/config/schema.d.ts +9 -0
- package/dist/src/core/config/schema.js +7 -0
- package/dist/src/core/config/types.d.ts +11 -0
- package/dist/src/core/council/runner.d.ts +10 -1
- package/dist/src/core/council/runner.js +25 -3
- package/dist/src/core/council/types.d.ts +7 -0
- package/dist/src/core/errors.d.ts +1 -1
- package/dist/src/core/errors.js +11 -0
- package/dist/src/core/logging/redaction.d.ts +13 -0
- package/dist/src/core/logging/redaction.js +20 -0
- package/dist/src/core/migrate/schema-validator.js +15 -1
- package/dist/src/core/phases/static-rules.d.ts +5 -1
- package/dist/src/core/phases/static-rules.js +2 -5
- package/dist/src/core/run-state/budget.d.ts +88 -0
- package/dist/src/core/run-state/budget.js +141 -0
- package/dist/src/core/run-state/cli-internal.d.ts +21 -0
- package/dist/src/core/run-state/cli-internal.js +174 -0
- package/dist/src/core/run-state/events.d.ts +59 -0
- package/dist/src/core/run-state/events.js +504 -0
- package/dist/src/core/run-state/lock.d.ts +61 -0
- package/dist/src/core/run-state/lock.js +206 -0
- package/dist/src/core/run-state/phase-context.d.ts +60 -0
- package/dist/src/core/run-state/phase-context.js +108 -0
- package/dist/src/core/run-state/phase-registry.d.ts +137 -0
- package/dist/src/core/run-state/phase-registry.js +162 -0
- package/dist/src/core/run-state/phase-runner.d.ts +80 -0
- package/dist/src/core/run-state/phase-runner.js +447 -0
- package/dist/src/core/run-state/provider-readback.d.ts +130 -0
- package/dist/src/core/run-state/provider-readback.js +426 -0
- package/dist/src/core/run-state/replay-decision.d.ts +69 -0
- package/dist/src/core/run-state/replay-decision.js +144 -0
- package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
- package/dist/src/core/run-state/resolve-engine.js +190 -0
- package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
- package/dist/src/core/run-state/resume-preflight.js +116 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
- package/dist/src/core/run-state/runs.d.ts +57 -0
- package/dist/src/core/run-state/runs.js +288 -0
- package/dist/src/core/run-state/snapshot.d.ts +14 -0
- package/dist/src/core/run-state/snapshot.js +114 -0
- package/dist/src/core/run-state/state.d.ts +40 -0
- package/dist/src/core/run-state/state.js +164 -0
- package/dist/src/core/run-state/types.d.ts +278 -0
- package/dist/src/core/run-state/types.js +13 -0
- package/dist/src/core/run-state/ulid.d.ts +11 -0
- package/dist/src/core/run-state/ulid.js +95 -0
- package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
- package/dist/src/core/schema-alignment/extractor/index.js +2 -2
- package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
- package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
- package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
- package/dist/src/core/schema-alignment/git-history.js +53 -0
- package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
- package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
- package/package.json +2 -1
- package/scripts/autoregress.ts +1 -1
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- package/skills/simplify-ui/SKILL.md +103 -0
- package/skills/ui/SKILL.md +117 -0
- package/skills/ui-ux-pro-max/SKILL.md +90 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
// src/adapters/deploy/render.ts
|
|
2
|
+
//
|
|
3
|
+
// First-class Render deploy adapter. Phase 2 of the v5.6 spec.
|
|
4
|
+
//
|
|
5
|
+
// Implements `deploy()` (POST a new deploy on a configured service, then
|
|
6
|
+
// poll until terminal) and `status()` (one-shot GET). Log streaming is
|
|
7
|
+
// Phase 3 (Render uses REST polling, lands later) and rollback is Phase 4
|
|
8
|
+
// (simulated by re-deploying the previous successful commit — Render has
|
|
9
|
+
// no native rollback verb).
|
|
10
|
+
//
|
|
11
|
+
// All HTTP calls go through an injectable `fetchImpl` so unit tests never
|
|
12
|
+
// hit the real Render API. The endpoint shapes below mirror the Render REST
|
|
13
|
+
// API as documented at https://api-docs.render.com/.
|
|
14
|
+
//
|
|
15
|
+
// Phase 5 (v5.6) consolidated `fetchWithRetry` and `safeReadBody` into the
|
|
16
|
+
// shared `_http.ts` module — see that file's header for the rationale.
|
|
17
|
+
// Error-mapping (`assertOkOrThrow`) stays per-adapter because each one
|
|
18
|
+
// composes a different error message and reads a different request-id
|
|
19
|
+
// header.
|
|
20
|
+
//
|
|
21
|
+
// Spec: docs/specs/v5.6-fly-render-adapters.md
|
|
22
|
+
import { GuardrailError } from "../../core/errors.js";
|
|
23
|
+
import { redactLogLines } from "../../core/logging/redaction.js";
|
|
24
|
+
import { fetchWithRetry, safeReadBody } from "./_http.js";
|
|
25
|
+
const RENDER_API_BASE = 'https://api.render.com';
|
|
26
|
+
const RENDER_DASHBOARD_BASE = 'https://dashboard.render.com';
|
|
27
|
+
const RENDER_TOKEN_DOC_URL = 'https://dashboard.render.com/u/settings#api-keys';
|
|
28
|
+
/**
|
|
29
|
+
* Render deploy adapter.
|
|
30
|
+
*
|
|
31
|
+
* Construct once per pipeline run. The adapter is stateless across calls —
|
|
32
|
+
* all configuration (token, serviceId, clearCache) is captured at
|
|
33
|
+
* construction time. Per the v5.6 spec, only `deploy()` and `status()` are
|
|
34
|
+
* wired in Phase 2; `streamLogs` (REST polling) and `rollback` (simulated
|
|
35
|
+
* via re-deploy) land in Phases 3 and 4 respectively.
|
|
36
|
+
*/
|
|
37
|
+
export class RenderDeployAdapter {
|
|
38
|
+
name = 'render';
|
|
39
|
+
capabilities = {
|
|
40
|
+
streamMode: 'polling',
|
|
41
|
+
nativeRollback: false,
|
|
42
|
+
};
|
|
43
|
+
token;
|
|
44
|
+
serviceId;
|
|
45
|
+
clearCache;
|
|
46
|
+
pollIntervalMs;
|
|
47
|
+
maxPollMs;
|
|
48
|
+
fetchImpl;
|
|
49
|
+
sleep;
|
|
50
|
+
now;
|
|
51
|
+
redactionPatterns;
|
|
52
|
+
logPollIntervalMs;
|
|
53
|
+
constructor(opts) {
|
|
54
|
+
const token = opts.token ?? process.env.RENDER_API_KEY;
|
|
55
|
+
if (!token) {
|
|
56
|
+
throw new GuardrailError(`Render deploy adapter requires RENDER_API_KEY. Create one at ${RENDER_TOKEN_DOC_URL}`, { code: 'auth', provider: 'render' });
|
|
57
|
+
}
|
|
58
|
+
if (!opts.serviceId) {
|
|
59
|
+
throw new GuardrailError('Render deploy adapter requires `serviceId` (Render service ID, e.g. srv-abc123)', { code: 'invalid_config', provider: 'render' });
|
|
60
|
+
}
|
|
61
|
+
this.token = token;
|
|
62
|
+
this.serviceId = opts.serviceId;
|
|
63
|
+
this.clearCache = opts.clearCache ?? 'do_not_clear';
|
|
64
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? 2000;
|
|
65
|
+
this.maxPollMs = opts.maxPollMs ?? 15 * 60 * 1000;
|
|
66
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
67
|
+
this.sleep = opts.sleepImpl ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
68
|
+
this.now = opts.nowImpl ?? Date.now;
|
|
69
|
+
this.redactionPatterns = opts.redactionPatterns;
|
|
70
|
+
this.logPollIntervalMs = opts.logPollIntervalMs ?? 2000;
|
|
71
|
+
}
|
|
72
|
+
async deploy(input) {
|
|
73
|
+
const start = this.now();
|
|
74
|
+
const url = `${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/deploys`;
|
|
75
|
+
const body = {
|
|
76
|
+
clearCache: this.clearCache,
|
|
77
|
+
};
|
|
78
|
+
// Render accepts `commitId` to deploy a specific commit — useful both
|
|
79
|
+
// for normal deploys driven by a SHA and for the eventual Phase 4
|
|
80
|
+
// simulated-rollback path that re-deploys a previous commit.
|
|
81
|
+
if (input.commitSha)
|
|
82
|
+
body.commitId = input.commitSha;
|
|
83
|
+
const res = await fetchWithRetry(this.fetchImpl, url, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: this.headers(),
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
signal: input.signal,
|
|
88
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
89
|
+
await this.assertOkOrThrow(res, 'create deploy');
|
|
90
|
+
const created = (await res.json());
|
|
91
|
+
if (!created.id) {
|
|
92
|
+
throw new GuardrailError(`Render returned no deploy id (got: ${JSON.stringify(created).slice(0, 200)})`, { code: 'adapter_bug', provider: 'render' });
|
|
93
|
+
}
|
|
94
|
+
// Fire onDeployStart so callers can subscribe to side-channel work
|
|
95
|
+
// (log streaming once Phase 3 lands) in parallel with polling. Wrap in
|
|
96
|
+
// try/catch — a buggy callback must not crash the deploy.
|
|
97
|
+
try {
|
|
98
|
+
input.onDeployStart?.(created.id);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* swallow — observability concern only */
|
|
102
|
+
}
|
|
103
|
+
return this.pollUntilTerminal(created.id, start, input.signal);
|
|
104
|
+
}
|
|
105
|
+
async status(input) {
|
|
106
|
+
const start = this.now();
|
|
107
|
+
// Render's API for fetching a single deploy is service-scoped — the
|
|
108
|
+
// shorthand /v1/deploys/{id} does NOT exist. Caught by Cursor Bugbot
|
|
109
|
+
// on PR #73 (HIGH).
|
|
110
|
+
const url = `${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/deploys/${encodeURIComponent(input.deployId)}`;
|
|
111
|
+
const res = await fetchWithRetry(this.fetchImpl, url, {
|
|
112
|
+
method: 'GET',
|
|
113
|
+
headers: this.headers(),
|
|
114
|
+
signal: input.signal,
|
|
115
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
116
|
+
await this.assertOkOrThrow(res, 'get deploy');
|
|
117
|
+
const data = (await res.json());
|
|
118
|
+
const result = this.shapeResult(input.deployId, data, data.status, this.now() - start);
|
|
119
|
+
return { ...result, deployId: input.deployId };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Phase 3 of v5.6 — REST-polling log stream for a Render deploy.
|
|
123
|
+
*
|
|
124
|
+
* Render has no WebSocket log endpoint (cf. v5.6 spec § "Render adapter →
|
|
125
|
+
* Logs" and capability metadata `streamMode: 'polling'`). This generator
|
|
126
|
+
* polls `GET /v1/services/{serviceId}/logs?deployId={id}&direction=forward
|
|
127
|
+
* &limit=100` every 2s while the deploy is `in-progress` and yields any
|
|
128
|
+
* new lines.
|
|
129
|
+
*
|
|
130
|
+
* Cursor invariant — keyed by `(timestamp, logId)`:
|
|
131
|
+
* - We track the most-recently-yielded `(ts, id)` pair as `cursor`.
|
|
132
|
+
* - On each poll, we discard every returned line whose `(ts, id)` is
|
|
133
|
+
* `<= cursor` (lexicographic on the pair, primary key timestamp). This
|
|
134
|
+
* handles two real cases:
|
|
135
|
+
* 1. Pagination overlap — Render's forward-direction list often
|
|
136
|
+
* repeats the last entry of the prior page as the first entry of
|
|
137
|
+
* the next. Without dedup we'd yield duplicates.
|
|
138
|
+
* 2. Same-millisecond entries — multiple log lines can share a `ts`.
|
|
139
|
+
* The secondary `id` ordering keeps them stable.
|
|
140
|
+
* - We never miss a line: `cursor` advances strictly monotonically, and
|
|
141
|
+
* the polling URL uses `direction=forward` so Render returns lines
|
|
142
|
+
* newer than (or equal to) our cursor's timestamp.
|
|
143
|
+
*
|
|
144
|
+
* Termination:
|
|
145
|
+
* - `signal.aborted` — exit immediately at the next await boundary.
|
|
146
|
+
* - Deploy status reaches a terminal state (live / build_failed /
|
|
147
|
+
* update_failed / canceled / deactivated) — drain one final poll for
|
|
148
|
+
* any tail lines, then exit.
|
|
149
|
+
* - Hard cap of `maxPollMs` ticks — same budget as `pollUntilTerminal`
|
|
150
|
+
* to avoid an infinite generator if status is stuck.
|
|
151
|
+
*
|
|
152
|
+
* Every yielded line's `text` is run through `redactLogLines()` before
|
|
153
|
+
* leaving the adapter.
|
|
154
|
+
*/
|
|
155
|
+
async *streamLogs(input) {
|
|
156
|
+
const logsUrl = (`${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/logs`
|
|
157
|
+
+ `?deployId=${encodeURIComponent(input.deployId)}`
|
|
158
|
+
+ `&direction=forward&limit=100`);
|
|
159
|
+
const statusUrl = `${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/deploys/${encodeURIComponent(input.deployId)}`;
|
|
160
|
+
const start = this.now();
|
|
161
|
+
let cursorTs = -1;
|
|
162
|
+
let cursorId = '';
|
|
163
|
+
let terminalSeen = false;
|
|
164
|
+
while (true) {
|
|
165
|
+
if (input.signal?.aborted)
|
|
166
|
+
return;
|
|
167
|
+
if (this.now() - start > this.maxPollMs)
|
|
168
|
+
return;
|
|
169
|
+
// 1. Fetch the next batch of log lines.
|
|
170
|
+
let logsRes;
|
|
171
|
+
try {
|
|
172
|
+
logsRes = await fetchWithRetry(this.fetchImpl, logsUrl, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers: this.headers(),
|
|
175
|
+
signal: input.signal,
|
|
176
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (err instanceof Error && err.name === 'AbortError')
|
|
180
|
+
return;
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
if (input.signal?.aborted)
|
|
184
|
+
return;
|
|
185
|
+
// 404 here = deploy ID typo or wrong service. Surface as a single
|
|
186
|
+
// warn line and stop — same shape as the Fly "lost stream" exit.
|
|
187
|
+
if (!logsRes.ok) {
|
|
188
|
+
// Re-use the assertOkOrThrow surface for a typed GuardrailError.
|
|
189
|
+
await this.assertOkOrThrow(logsRes, 'stream logs');
|
|
190
|
+
}
|
|
191
|
+
const logsData = (await logsRes.json());
|
|
192
|
+
const lines = Array.isArray(logsData?.logs) ? logsData.logs : [];
|
|
193
|
+
// Parse first, then sort by (ts, id) ascending before applying the
|
|
194
|
+
// cursor filter. Render's API does NOT guarantee that same-millisecond
|
|
195
|
+
// entries arrive in lexicographic id order — without this sort, an
|
|
196
|
+
// entry with an alphabetically-earlier id arriving AFTER a same-ts
|
|
197
|
+
// sibling would advance the cursor past it and silently drop it on
|
|
198
|
+
// the next pass. Caught by Cursor Bugbot on PR #75 (MEDIUM).
|
|
199
|
+
const parsedBatch = [];
|
|
200
|
+
for (const entry of lines) {
|
|
201
|
+
if (input.signal?.aborted)
|
|
202
|
+
return;
|
|
203
|
+
const parsed = parseRenderLogEntry(entry, this.now());
|
|
204
|
+
if (parsed)
|
|
205
|
+
parsedBatch.push(parsed);
|
|
206
|
+
}
|
|
207
|
+
parsedBatch.sort((a, b) => {
|
|
208
|
+
if (a.ts !== b.ts)
|
|
209
|
+
return a.ts - b.ts;
|
|
210
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
211
|
+
});
|
|
212
|
+
for (const parsed of parsedBatch) {
|
|
213
|
+
if (input.signal?.aborted)
|
|
214
|
+
return;
|
|
215
|
+
// Cursor compare: primary timestamp, secondary id. Strictly greater
|
|
216
|
+
// than previous cursor → yield + advance.
|
|
217
|
+
if (parsed.ts < cursorTs)
|
|
218
|
+
continue;
|
|
219
|
+
if (parsed.ts === cursorTs && parsed.id <= cursorId)
|
|
220
|
+
continue;
|
|
221
|
+
cursorTs = parsed.ts;
|
|
222
|
+
cursorId = parsed.id;
|
|
223
|
+
yield this.redactLine({ timestamp: parsed.ts, level: parsed.level, text: parsed.text });
|
|
224
|
+
}
|
|
225
|
+
// 2. After we've drained this poll, check if we already saw a terminal
|
|
226
|
+
// status on the previous tick — if so, this was the final tail-drain.
|
|
227
|
+
if (terminalSeen)
|
|
228
|
+
return;
|
|
229
|
+
// 3. Status check — same service-scoped endpoint as `pollUntilTerminal`.
|
|
230
|
+
let statusRes;
|
|
231
|
+
try {
|
|
232
|
+
statusRes = await fetchWithRetry(this.fetchImpl, statusUrl, {
|
|
233
|
+
method: 'GET',
|
|
234
|
+
headers: this.headers(),
|
|
235
|
+
signal: input.signal,
|
|
236
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (err instanceof Error && err.name === 'AbortError')
|
|
240
|
+
return;
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
if (input.signal?.aborted)
|
|
244
|
+
return;
|
|
245
|
+
if (statusRes.ok) {
|
|
246
|
+
const statusData = (await statusRes.json());
|
|
247
|
+
const s = statusData?.status;
|
|
248
|
+
if (s === 'live'
|
|
249
|
+
|| s === 'build_failed'
|
|
250
|
+
|| s === 'update_failed'
|
|
251
|
+
|| s === 'canceled'
|
|
252
|
+
|| s === 'deactivated') {
|
|
253
|
+
// Mark terminal — one more poll iteration drains tail lines, then
|
|
254
|
+
// the `terminalSeen` short-circuit above exits the loop.
|
|
255
|
+
terminalSeen = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// 4. Sleep until the next poll. Honor abort while waiting.
|
|
259
|
+
if (input.signal?.aborted)
|
|
260
|
+
return;
|
|
261
|
+
await this.sleep(this.logPollIntervalMs);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Phase 4 of v5.6 — simulated rollback for Render.
|
|
266
|
+
*
|
|
267
|
+
* Render has no native rollback verb, so we simulate by re-deploying a
|
|
268
|
+
* prior commit per spec § "Render adapter → Rollback":
|
|
269
|
+
*
|
|
270
|
+
* - List recent deploys: `GET /v1/services/{serviceId}/deploys?limit=20`.
|
|
271
|
+
* - Select the rollback target:
|
|
272
|
+
* - When `input.to` is set: look up that specific deploy in the list,
|
|
273
|
+
* read its `commit.id`, and re-deploy that commit. Deploy ID becomes
|
|
274
|
+
* the lookup key.
|
|
275
|
+
* - When `input.to` is unset: walk the list newest-first and pick the
|
|
276
|
+
* most recent deploy with `status === 'live'` *that is not the head*.
|
|
277
|
+
* The intent is "go back one" — the head is the deploy we'd be
|
|
278
|
+
* rolling back from.
|
|
279
|
+
* - Re-deploy via `POST /v1/services/{serviceId}/deploys` with `commitId`,
|
|
280
|
+
* then poll until terminal — re-uses the existing `deploy()`-style
|
|
281
|
+
* machinery via `redeployCommit()`.
|
|
282
|
+
*
|
|
283
|
+
* Throws `GuardrailError({ code: 'no_previous_deploy', provider: 'render' })`
|
|
284
|
+
* when no usable prior `live` deploy exists. Throws `not_found` when
|
|
285
|
+
* `input.to` references a deploy that isn't in the recent-20 window.
|
|
286
|
+
*
|
|
287
|
+
* Returns a `DeployResult` with:
|
|
288
|
+
* - `deployId` — the *new* deploy ID Render returned for the re-deploy.
|
|
289
|
+
* - `rolledBackTo` — the *prior* deploy ID we rolled back to.
|
|
290
|
+
* - `output` — human-readable summary of the swap (commits + IDs), redacted.
|
|
291
|
+
*/
|
|
292
|
+
async rollback(input) {
|
|
293
|
+
const start = this.now();
|
|
294
|
+
const deploys = await this.listDeploys(20, input.signal);
|
|
295
|
+
let priorDeployId;
|
|
296
|
+
let priorCommitId;
|
|
297
|
+
if (input.to) {
|
|
298
|
+
const match = deploys.find((d) => d.id === input.to);
|
|
299
|
+
if (!match) {
|
|
300
|
+
throw new GuardrailError(`Render deploy "${input.to}" not found in the last 20 deploys for service "${this.serviceId}" — cannot rollback`, { code: 'not_found', provider: 'render', step: 'rollback lookup' });
|
|
301
|
+
}
|
|
302
|
+
if (!match.commit?.id) {
|
|
303
|
+
throw new GuardrailError(`Render deploy "${input.to}" has no recorded commit id — cannot simulate rollback`, { code: 'invalid_config', provider: 'render', step: 'rollback lookup' });
|
|
304
|
+
}
|
|
305
|
+
priorDeployId = match.id;
|
|
306
|
+
priorCommitId = match.commit.id;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Walk newest-first. The first `live` deploy is the current head — we
|
|
310
|
+
// skip it and return the next. This matches the "go back one"
|
|
311
|
+
// semantics from the spec: the head is the deploy we're rolling
|
|
312
|
+
// BACK FROM, never the rollback target.
|
|
313
|
+
let sawHead = false;
|
|
314
|
+
for (const d of deploys) {
|
|
315
|
+
if (d.status !== 'live')
|
|
316
|
+
continue;
|
|
317
|
+
if (!sawHead) {
|
|
318
|
+
sawHead = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (!d.commit?.id)
|
|
322
|
+
continue;
|
|
323
|
+
priorDeployId = d.id;
|
|
324
|
+
priorCommitId = d.commit.id;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
if (!priorDeployId || !priorCommitId) {
|
|
328
|
+
throw new GuardrailError(`No previous live Render deploy with a commit id found for service "${this.serviceId}" to roll back to`, { code: 'no_previous_deploy', provider: 'render' });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const redeployed = await this.redeployCommit(priorCommitId, input.signal);
|
|
332
|
+
const rawOutput = `Render rollback simulated by re-deploying commit "${priorCommitId}" (prior deploy ${priorDeployId}) → new deploy ${redeployed.deployId ?? '<unknown>'}`;
|
|
333
|
+
return {
|
|
334
|
+
...redeployed,
|
|
335
|
+
// The prior deploy ID is the rollback target; deployId stays the
|
|
336
|
+
// newly-created deploy from the re-deploy POST. The CLI surfaces
|
|
337
|
+
// both in the PR-comment row.
|
|
338
|
+
rolledBackTo: priorDeployId,
|
|
339
|
+
durationMs: this.now() - start,
|
|
340
|
+
output: redactLogLines(rawOutput, this.redactionPatterns),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Private helper — re-uses the deploy() POST + poll machinery to redeploy
|
|
345
|
+
* a specific commit. Used by `rollback()` to reissue a prior commit.
|
|
346
|
+
*/
|
|
347
|
+
async redeployCommit(commitId, signal) {
|
|
348
|
+
const start = this.now();
|
|
349
|
+
const url = `${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/deploys`;
|
|
350
|
+
const body = {
|
|
351
|
+
clearCache: this.clearCache,
|
|
352
|
+
commitId,
|
|
353
|
+
};
|
|
354
|
+
const res = await fetchWithRetry(this.fetchImpl, url, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: this.headers(),
|
|
357
|
+
body: JSON.stringify(body),
|
|
358
|
+
signal,
|
|
359
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
360
|
+
await this.assertOkOrThrow(res, 'create deploy (rollback)');
|
|
361
|
+
const created = (await res.json());
|
|
362
|
+
if (!created.id) {
|
|
363
|
+
throw new GuardrailError(`Render returned no deploy id during rollback (got: ${JSON.stringify(created).slice(0, 200)})`, { code: 'adapter_bug', provider: 'render' });
|
|
364
|
+
}
|
|
365
|
+
return this.pollUntilTerminal(created.id, start, signal);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* List the most recent deploys for the configured service. Newest-first.
|
|
369
|
+
* `limit` caps the result set — defaults to 20 (the spec's recommended
|
|
370
|
+
* window for the rollback lookup). Each entry includes `id`, `commit.id`,
|
|
371
|
+
* and `status` per the Render REST API.
|
|
372
|
+
*/
|
|
373
|
+
async listDeploys(limit = 20, signal) {
|
|
374
|
+
const url = `${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/deploys`
|
|
375
|
+
+ `?limit=${encodeURIComponent(String(limit))}`;
|
|
376
|
+
const res = await fetchWithRetry(this.fetchImpl, url, {
|
|
377
|
+
method: 'GET',
|
|
378
|
+
headers: this.headers(),
|
|
379
|
+
signal,
|
|
380
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
381
|
+
await this.assertOkOrThrow(res, 'list deploys');
|
|
382
|
+
const data = (await res.json());
|
|
383
|
+
// Render historically returned a top-level array on /deploys; newer
|
|
384
|
+
// versions wrap entries in `[{deploy: {...}, cursor: '...'}]`. Accept
|
|
385
|
+
// both shapes — fall back to a bare-array if neither envelope matches.
|
|
386
|
+
if (Array.isArray(data)) {
|
|
387
|
+
// Could be either RenderDeployResponse[] OR [{deploy: ...}].
|
|
388
|
+
const first = data[0];
|
|
389
|
+
if (first && typeof first === 'object' && 'deploy' in first) {
|
|
390
|
+
const wrapped = data;
|
|
391
|
+
return wrapped.map((entry) => entry.deploy).filter(Boolean);
|
|
392
|
+
}
|
|
393
|
+
return data;
|
|
394
|
+
}
|
|
395
|
+
if (Array.isArray(data?.deploys)) {
|
|
396
|
+
return data.deploys ?? [];
|
|
397
|
+
}
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
// private helpers
|
|
402
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
403
|
+
/** Apply the adapter's redaction patterns to a log line's `text` field. */
|
|
404
|
+
redactLine(line) {
|
|
405
|
+
return { ...line, text: redactLogLines(line.text, this.redactionPatterns) };
|
|
406
|
+
}
|
|
407
|
+
async pollUntilTerminal(deployId, start, signal) {
|
|
408
|
+
// Service-scoped path — see comment in `status()`.
|
|
409
|
+
const url = `${RENDER_API_BASE}/v1/services/${encodeURIComponent(this.serviceId)}/deploys/${encodeURIComponent(deployId)}`;
|
|
410
|
+
while (true) {
|
|
411
|
+
if (signal?.aborted) {
|
|
412
|
+
return { status: 'in-progress', deployId, durationMs: this.now() - start };
|
|
413
|
+
}
|
|
414
|
+
if (this.now() - start > this.maxPollMs) {
|
|
415
|
+
return {
|
|
416
|
+
status: 'in-progress',
|
|
417
|
+
deployId,
|
|
418
|
+
durationMs: this.now() - start,
|
|
419
|
+
buildLogsUrl: this.buildLogsUrl(deployId),
|
|
420
|
+
output: redactLogLines(`Render deploy still in progress after ${this.maxPollMs}ms — check ${this.buildLogsUrl(deployId)}`, this.redactionPatterns),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const res = await fetchWithRetry(this.fetchImpl, url, {
|
|
424
|
+
method: 'GET',
|
|
425
|
+
headers: this.headers(),
|
|
426
|
+
signal,
|
|
427
|
+
}, { sleepImpl: this.sleep, provider: 'render' });
|
|
428
|
+
await this.assertOkOrThrow(res, 'poll deploy');
|
|
429
|
+
const data = (await res.json());
|
|
430
|
+
const status = data.status;
|
|
431
|
+
if (status === 'live'
|
|
432
|
+
|| status === 'build_failed'
|
|
433
|
+
|| status === 'update_failed'
|
|
434
|
+
|| status === 'canceled'
|
|
435
|
+
|| status === 'deactivated') {
|
|
436
|
+
return this.shapeResult(deployId, data, status, this.now() - start);
|
|
437
|
+
}
|
|
438
|
+
await this.sleep(this.pollIntervalMs);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
shapeResult(deployId, data, status, durationMs) {
|
|
442
|
+
// Map Render's eight-state vocabulary onto our pass/fail/in-progress
|
|
443
|
+
// tri-state. `live` is the only success terminal; `deactivated`,
|
|
444
|
+
// `build_failed`, `update_failed`, `canceled` are failure terminals;
|
|
445
|
+
// everything else is interim.
|
|
446
|
+
const resultStatus = status === 'live'
|
|
447
|
+
? 'pass'
|
|
448
|
+
: status === 'build_failed'
|
|
449
|
+
|| status === 'update_failed'
|
|
450
|
+
|| status === 'canceled'
|
|
451
|
+
|| status === 'deactivated'
|
|
452
|
+
? 'fail'
|
|
453
|
+
: 'in-progress';
|
|
454
|
+
// Apply redaction to the human-readable output line. Real-world Render
|
|
455
|
+
// logs often echo back env vars and tokens; we never want those landing
|
|
456
|
+
// in PR-comment bodies. (Spec § "Log redaction".)
|
|
457
|
+
const commitInfo = data.commit?.id ? ` commit=${data.commit.id}` : '';
|
|
458
|
+
const rawOutput = status ? `Render deploy ${deployId}: status=${status}${commitInfo}` : undefined;
|
|
459
|
+
return {
|
|
460
|
+
status: resultStatus,
|
|
461
|
+
deployId,
|
|
462
|
+
buildLogsUrl: this.buildLogsUrl(deployId),
|
|
463
|
+
durationMs,
|
|
464
|
+
output: rawOutput !== undefined ? redactLogLines(rawOutput, this.redactionPatterns) : undefined,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
headers() {
|
|
468
|
+
return {
|
|
469
|
+
Authorization: `Bearer ${this.token}`,
|
|
470
|
+
'Content-Type': 'application/json',
|
|
471
|
+
Accept: 'application/json',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
buildLogsUrl(deployId) {
|
|
475
|
+
return `${RENDER_DASHBOARD_BASE}/web/${encodeURIComponent(this.serviceId)}/deploys/${encodeURIComponent(deployId)}`;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* HTTP-status-keyed error mapper. Per v5.6 spec:
|
|
479
|
+
*
|
|
480
|
+
* | Status | ErrorCode |
|
|
481
|
+
* |---|---|
|
|
482
|
+
* | 401 / 403 | `auth` |
|
|
483
|
+
* | 404 | `not_found` |
|
|
484
|
+
* | 422 / 400 | `invalid_config` |
|
|
485
|
+
* | 5xx | `transient_network` (retryable) |
|
|
486
|
+
* | other 4xx | `adapter_bug` |
|
|
487
|
+
*
|
|
488
|
+
* Render echoes a request-id on the `x-request-id` header on most
|
|
489
|
+
* responses. We capture it into `details.renderRequestId` whenever
|
|
490
|
+
* present so support tickets can quote it back to Render.
|
|
491
|
+
*/
|
|
492
|
+
async assertOkOrThrow(res, step) {
|
|
493
|
+
if (res.ok)
|
|
494
|
+
return;
|
|
495
|
+
const bodyText = await safeReadBody(res);
|
|
496
|
+
const requestId = readRenderRequestId(res);
|
|
497
|
+
const details = { status: res.status };
|
|
498
|
+
if (requestId)
|
|
499
|
+
details.renderRequestId = requestId;
|
|
500
|
+
if (res.status === 401 || res.status === 403) {
|
|
501
|
+
throw new GuardrailError(`Render auth failed (${res.status}) on ${step} — check RENDER_API_KEY scope for service "${this.serviceId}". Regenerate at ${RENDER_TOKEN_DOC_URL}: ${bodyText}`, { code: 'auth', provider: 'render', step, details });
|
|
502
|
+
}
|
|
503
|
+
if (res.status === 404) {
|
|
504
|
+
throw new GuardrailError(`Render resource not found (${res.status}) on ${step} — service ID "${this.serviceId}" may be wrong, or the deploy ID belongs to a different service${requestId ? ` (x-request-id: ${requestId})` : ''}: ${bodyText}`, { code: 'not_found', provider: 'render', step, details });
|
|
505
|
+
}
|
|
506
|
+
if (res.status === 422 || res.status === 400) {
|
|
507
|
+
throw new GuardrailError(`Render rejected the request (${res.status}) on ${step} — likely a malformed serviceId, invalid clearCache value, or unknown commitId: ${bodyText}`, { code: 'invalid_config', provider: 'render', step, details });
|
|
508
|
+
}
|
|
509
|
+
if (res.status >= 500 && res.status < 600) {
|
|
510
|
+
throw new GuardrailError(`Render API server error (${res.status}) on ${step}: ${bodyText}`, { code: 'transient_network', provider: 'render', step, details, retryable: true });
|
|
511
|
+
}
|
|
512
|
+
throw new GuardrailError(`Render API error (${res.status}) on ${step}: ${bodyText}`, { code: 'adapter_bug', provider: 'render', step, details });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Pull `x-request-id` (case-insensitive) off the response. Render echoes
|
|
517
|
+
* this header on most API responses; capturing it into
|
|
518
|
+
* `GuardrailError.details.renderRequestId` lets users quote it back when
|
|
519
|
+
* filing support tickets.
|
|
520
|
+
*
|
|
521
|
+
* Falls back to `null` when `headers.get` is unavailable (e.g. a stubbed
|
|
522
|
+
* Response in tests that doesn't implement Headers).
|
|
523
|
+
*/
|
|
524
|
+
function readRenderRequestId(res) {
|
|
525
|
+
const headers = res.headers;
|
|
526
|
+
if (!headers || typeof headers.get !== 'function')
|
|
527
|
+
return null;
|
|
528
|
+
return headers.get('x-request-id') ?? headers.get('X-Request-Id') ?? null;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Parse a single Render log entry into our cursor-friendly tuple. Returns
|
|
532
|
+
* `null` for entries that have no usable text (we never yield empty lines)
|
|
533
|
+
* or no usable timestamp (the cursor invariant requires `ts`).
|
|
534
|
+
*/
|
|
535
|
+
function parseRenderLogEntry(entry, fallbackTs) {
|
|
536
|
+
const text = typeof entry.message === 'string'
|
|
537
|
+
? entry.message
|
|
538
|
+
: typeof entry.text === 'string' ? entry.text : '';
|
|
539
|
+
if (!text)
|
|
540
|
+
return null;
|
|
541
|
+
let ts = fallbackTs;
|
|
542
|
+
if (typeof entry.timestamp === 'string' && entry.timestamp.length > 0) {
|
|
543
|
+
const parsed = Date.parse(entry.timestamp);
|
|
544
|
+
if (!Number.isNaN(parsed))
|
|
545
|
+
ts = parsed;
|
|
546
|
+
}
|
|
547
|
+
const id = typeof entry.id === 'string' ? entry.id : '';
|
|
548
|
+
return { ts, id, level: entry.level, text };
|
|
549
|
+
}
|
|
550
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -33,9 +33,19 @@ export interface DeployInput {
|
|
|
33
33
|
* before the platform reached a terminal state — the deploy may still finish
|
|
34
34
|
* later. The adapter does NOT auto-resume in Phase 1; the caller can re-poll
|
|
35
35
|
* via `status({ deployId })`.
|
|
36
|
+
*
|
|
37
|
+
* `fail_rolled_back` and `fail_rollback_failed` (Phase 4 of v5.6) describe
|
|
38
|
+
* the bounded auto-rollback outcomes. They both mean "the deploy itself
|
|
39
|
+
* succeeded but the post-deploy health check failed"; the suffix indicates
|
|
40
|
+
* whether the subsequent rollback attempt succeeded (`_rolled_back`) or
|
|
41
|
+
* failed (`_rollback_failed`). Adapters never set these directly — they are
|
|
42
|
+
* stamped onto the `DeployResult` by the CLI orchestration in
|
|
43
|
+
* `src/cli/deploy.ts` after `rollback()` returns. Plain `fail` continues to
|
|
44
|
+
* cover deploy-itself failures and the "no rollback configured / not
|
|
45
|
+
* supported" branches so existing consumers keep working.
|
|
36
46
|
*/
|
|
37
47
|
export interface DeployResult {
|
|
38
|
-
status: 'pass' | 'fail' | 'in-progress';
|
|
48
|
+
status: 'pass' | 'fail' | 'in-progress' | 'fail_rolled_back' | 'fail_rollback_failed';
|
|
39
49
|
/** Adapter-native deploy ID. Vercel uses `dpl_xxx`. Empty for generic when stdout has no extractable URL. */
|
|
40
50
|
deployId?: string;
|
|
41
51
|
/** Public URL of the deploy (e.g. `https://my-app-abc.vercel.app`). */
|
|
@@ -105,6 +115,34 @@ export interface DeployLogLine {
|
|
|
105
115
|
/** Log text, no trailing newline. */
|
|
106
116
|
text: string;
|
|
107
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Self-described capability surface for an adapter.
|
|
120
|
+
*
|
|
121
|
+
* Codex review of v5.6 (#4) flagged that Render's REST polling and Fly's
|
|
122
|
+
* WebSocket streaming offer materially different log-streaming experiences,
|
|
123
|
+
* and pretending they're equivalent degrades user trust. CLI surfaces (and
|
|
124
|
+
* downstream consumers) inspect this struct to print a one-line notice when
|
|
125
|
+
* `--watch` is invoked against a `polling`-mode adapter, and to choose between
|
|
126
|
+
* native vs simulated rollback messaging in the PR comment.
|
|
127
|
+
*
|
|
128
|
+
* The field is optional — adapters that don't declare capabilities are
|
|
129
|
+
* treated as `streamMode: 'none'`, `nativeRollback: false` for safety.
|
|
130
|
+
*/
|
|
131
|
+
export interface DeployAdapterCapabilities {
|
|
132
|
+
/**
|
|
133
|
+
* How `streamLogs()` (when implemented) delivers lines:
|
|
134
|
+
* - `websocket` — real-time, push-based, near-zero gap between line emit and consumer receive
|
|
135
|
+
* - `polling` — REST-paginated polling cursor, lines may arrive in batches with short gaps
|
|
136
|
+
* - `none` — no log-streaming surface at all
|
|
137
|
+
*/
|
|
138
|
+
streamMode?: 'websocket' | 'polling' | 'none';
|
|
139
|
+
/**
|
|
140
|
+
* `true` when the platform exposes a single "promote prior release" verb
|
|
141
|
+
* (Vercel's `/promote`, Fly's `/rollback`); `false` when rollback must be
|
|
142
|
+
* simulated by re-deploying a previous successful image/commit (Render).
|
|
143
|
+
*/
|
|
144
|
+
nativeRollback?: boolean;
|
|
145
|
+
}
|
|
108
146
|
/**
|
|
109
147
|
* The DeployAdapter contract.
|
|
110
148
|
*
|
|
@@ -115,6 +153,12 @@ export interface DeployLogLine {
|
|
|
115
153
|
export interface DeployAdapter {
|
|
116
154
|
/** Stable identifier — surfaced in CLI output and logs. */
|
|
117
155
|
readonly name: string;
|
|
156
|
+
/**
|
|
157
|
+
* Optional self-description of the adapter's streaming + rollback shape.
|
|
158
|
+
* See {@link DeployAdapterCapabilities} for semantics. CLI consumers use
|
|
159
|
+
* this to decide whether to print the polling-mode notice on `--watch`.
|
|
160
|
+
*/
|
|
161
|
+
readonly capabilities?: DeployAdapterCapabilities;
|
|
118
162
|
deploy(input: DeployInput): Promise<DeployResult>;
|
|
119
163
|
status?(input: DeployStatusInput): Promise<DeployStatusResult>;
|
|
120
164
|
rollback?(input: DeployRollbackInput): Promise<DeployResult>;
|
|
@@ -137,14 +181,34 @@ export interface DeployAdapter {
|
|
|
137
181
|
* The factory in `./index.ts` enforces these rules at construction time.
|
|
138
182
|
*/
|
|
139
183
|
export interface DeployConfig {
|
|
140
|
-
/**
|
|
141
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Which adapter to use. v5.4 shipped `vercel` + `generic`; v5.6 Phase 1
|
|
186
|
+
* adds `fly`; v5.6 Phase 2 adds `render`.
|
|
187
|
+
*/
|
|
188
|
+
adapter: 'vercel' | 'fly' | 'render' | 'generic';
|
|
142
189
|
/** Vercel project ID or slug. Required when `adapter === 'vercel'`. */
|
|
143
190
|
project?: string;
|
|
144
191
|
/** Vercel team ID for team accounts. Optional. */
|
|
145
192
|
team?: string;
|
|
146
193
|
/** Deploy target. Default: `production`. */
|
|
147
194
|
target?: 'production' | 'preview';
|
|
195
|
+
/** Fly app slug. Required when `adapter === 'fly'`. */
|
|
196
|
+
app?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Pre-pushed image reference, e.g. `registry.fly.io/my-app:deployment-01`.
|
|
199
|
+
* Required when `adapter === 'fly'` — the Fly adapter does not build the
|
|
200
|
+
* image; pushing is the user's responsibility (`fly deploy --build-only --push`).
|
|
201
|
+
*/
|
|
202
|
+
image?: string;
|
|
203
|
+
/** Optional Fly region pin (e.g. `ord`). Falls back to the app's default region. */
|
|
204
|
+
region?: string;
|
|
205
|
+
/** Render service ID (e.g. `srv-abc123`). Required when `adapter === 'render'`. */
|
|
206
|
+
serviceId?: string;
|
|
207
|
+
/**
|
|
208
|
+
* Whether Render should clear the build cache before deploying. Optional,
|
|
209
|
+
* default `'do_not_clear'`. Maps directly to the Render API body field.
|
|
210
|
+
*/
|
|
211
|
+
clearCache?: 'do_not_clear' | 'clear';
|
|
148
212
|
/** Shell command to run for the deploy (e.g. `vercel --prod`). Required when `adapter === 'generic'`. */
|
|
149
213
|
deployCommand?: string;
|
|
150
214
|
/** Stream build logs to stderr in real time. Phase 2. */
|