insta 0.0.81 → 0.0.83
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/dist/build-logs.js +49 -9
- package/dist/deploy-archive.js +64 -55
- package/package.json +1 -1
package/dist/build-logs.js
CHANGED
|
@@ -6,6 +6,11 @@ const terminal = new Set(['succeeded', 'failed', 'canceled', 'unknown', 'live'])
|
|
|
6
6
|
const entryKey = (step, entry) => createHash('sha256').update(JSON.stringify([step, entry.timestamp, entry.message])).digest('hex');
|
|
7
7
|
class LogsNotReady extends Error {
|
|
8
8
|
}
|
|
9
|
+
// Compute emits UTC RFC3339Nano; Date.parse discards submillisecond ordering.
|
|
10
|
+
function completionKey(value) {
|
|
11
|
+
const [seconds, fraction = ''] = value.slice(0, -1).split('.');
|
|
12
|
+
return `${seconds}.${fraction.padEnd(9, '0')}`;
|
|
13
|
+
}
|
|
9
14
|
export async function readBuildLogs(api, projectId, source, buildId, signal = AbortSignal.timeout(30_000), follow) {
|
|
10
15
|
let bytes = 0;
|
|
11
16
|
let requests = 0;
|
|
@@ -19,7 +24,20 @@ export async function readBuildLogs(api, projectId, source, buildId, signal = Ab
|
|
|
19
24
|
if (++requests > 200)
|
|
20
25
|
throw new Error('build logs exceed the per-read page limit');
|
|
21
26
|
const params = new URLSearchParams({ ...(step ? { step } : {}), ...(cursor ? { cursor } : {}) });
|
|
22
|
-
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
const request = new AbortController();
|
|
29
|
+
const abort = () => request.abort(signal.reason);
|
|
30
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
31
|
+
const timeout = setTimeout(() => request.abort(new DOMException('build log request timed out', 'TimeoutError')), 20_000);
|
|
32
|
+
let body;
|
|
33
|
+
try {
|
|
34
|
+
const response = await api.rawRequest('GET', `/projects/${projectId}/builds/${source}/${encodeURIComponent(buildId)}/logs?${params}`, undefined, { signal: request.signal });
|
|
35
|
+
body = response.body;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
signal.removeEventListener('abort', abort);
|
|
40
|
+
}
|
|
23
41
|
if (!body || !['ready', 'pending', 'unsupported', 'unavailable'].includes(body.state) || !Array.isArray(body.steps) || !Array.isArray(body.entries))
|
|
24
42
|
throw new Error('invalid build log response');
|
|
25
43
|
bytes += Buffer.byteLength(JSON.stringify(body));
|
|
@@ -60,7 +78,18 @@ export async function readBuildLogs(api, projectId, source, buildId, signal = Ab
|
|
|
60
78
|
const first = stepPages[0];
|
|
61
79
|
if (stepPages.some((page) => page.state !== first.state))
|
|
62
80
|
throw new LogsNotReady('build steps are temporarily unavailable');
|
|
63
|
-
const
|
|
81
|
+
const byDigest = new Map();
|
|
82
|
+
for (const step of stepPages.flatMap((page) => page.steps)) {
|
|
83
|
+
const previous = byDigest.get(step.digest);
|
|
84
|
+
if (!previous) {
|
|
85
|
+
byDigest.set(step.digest, step);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const [older, newer] = previous.completedAt && (!step.completedAt || completionKey(previous.completedAt) > completionKey(step.completedAt))
|
|
89
|
+
? [step, previous] : [previous, step];
|
|
90
|
+
byDigest.set(step.digest, { ...older, ...newer, hasLogs: older.hasLogs || newer.hasLogs, error: newer.error });
|
|
91
|
+
}
|
|
92
|
+
const steps = [...byDigest.values()];
|
|
64
93
|
follow?.emit({ ...first, steps, output: [] });
|
|
65
94
|
const output = [];
|
|
66
95
|
for (const step of steps) {
|
|
@@ -127,20 +156,24 @@ export function archiveLogWatcher(api, projectId, write, wait = sleep) {
|
|
|
127
156
|
const follow = { tails: new Map(), emit: snapshot => printer.print(snapshot, write) };
|
|
128
157
|
let warned = '';
|
|
129
158
|
let unavailable = false;
|
|
130
|
-
return async (buildId, finished, remainingMs = 30_000) => {
|
|
159
|
+
return async (buildId, finished, remainingMs = 30_000, cancelSignal) => {
|
|
131
160
|
if (unavailable)
|
|
132
161
|
return;
|
|
133
|
-
const
|
|
162
|
+
const started = Date.now();
|
|
163
|
+
const deadline = started + remainingMs;
|
|
164
|
+
const refreshDeadline = started + Math.min(remainingMs, finished ? 18_000 : remainingMs);
|
|
134
165
|
for (let attempt = 0; attempt < (finished ? 6 : 1); attempt++) {
|
|
135
166
|
if (attempt > 0)
|
|
136
|
-
await wait(Math.min(3000, Math.max(0,
|
|
137
|
-
const
|
|
167
|
+
await wait(Math.min(3000, Math.max(0, refreshDeadline - Date.now())));
|
|
168
|
+
const finalRead = finished && (attempt === 5 || Date.now() >= refreshDeadline);
|
|
169
|
+
const remaining = Math.min(deadline - Date.now(), finalRead ? 30_000 : refreshDeadline - Date.now());
|
|
138
170
|
if (remaining <= 0)
|
|
139
171
|
return;
|
|
140
|
-
if (finished && (attempt === 0 ||
|
|
172
|
+
if (finished && (attempt === 0 || finalRead))
|
|
141
173
|
follow.tails.clear();
|
|
174
|
+
const signal = cancelSignal ?? AbortSignal.timeout(remaining);
|
|
142
175
|
try {
|
|
143
|
-
const snapshot = await readBuildLogs(api, projectId, 'archive', buildId,
|
|
176
|
+
const snapshot = await readBuildLogs(api, projectId, 'archive', buildId, signal, follow);
|
|
144
177
|
if (snapshot.state === 'unsupported') {
|
|
145
178
|
printer.finishLine(write);
|
|
146
179
|
write('Build logs are not supported for this build provider.\n');
|
|
@@ -152,14 +185,21 @@ export function archiveLogWatcher(api, projectId, write, wait = sleep) {
|
|
|
152
185
|
warned = '';
|
|
153
186
|
}
|
|
154
187
|
catch (error) {
|
|
188
|
+
if (cancelSignal?.aborted)
|
|
189
|
+
return;
|
|
190
|
+
if (finished && signal.aborted && !finalRead && Date.now() < deadline)
|
|
191
|
+
continue;
|
|
155
192
|
if (error instanceof ApiError && error.status === 400)
|
|
156
193
|
follow.tails.clear();
|
|
157
194
|
printer.finishLine(write);
|
|
158
|
-
const
|
|
195
|
+
const reason = signal.aborted || (error instanceof Error && error.name === 'TimeoutError') ? ' (timed out)' : error instanceof ApiError ? ` (HTTP ${error.status})` : '';
|
|
196
|
+
const message = `Could not read build logs${reason}. Retry with: insta build-logs ${buildId}\n`;
|
|
159
197
|
if (warned !== message)
|
|
160
198
|
write(message);
|
|
161
199
|
warned = message;
|
|
162
200
|
}
|
|
201
|
+
if (finalRead)
|
|
202
|
+
return;
|
|
163
203
|
}
|
|
164
204
|
};
|
|
165
205
|
}
|
package/dist/deploy-archive.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import { handleApproval } from './util.js';
|
|
2
3
|
export function archiveBuildSpec(hasDockerfile) {
|
|
3
4
|
return hasDockerfile ? { type: 'dockerfile' } : { type: 'nixpacks' };
|
|
@@ -84,64 +85,72 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
|
|
|
84
85
|
log('resuming the deploy this archive already started');
|
|
85
86
|
const deadline = now() + DEPLOY_DEADLINE_MS;
|
|
86
87
|
const overdue = () => new Error(`the deploy did not finish within ${Math.round(DEPLOY_DEADLINE_MS / 60000)} minutes — check \`insta status\` or re-run`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const remaining = deadline - now();
|
|
93
|
-
if (remaining <= 0)
|
|
94
|
-
throw overdue();
|
|
95
|
-
const res = await api.rawRequest('GET', `/projects/${projectId}/archive-deploys/${encodeURIComponent(operationId)}`, undefined, {
|
|
96
|
-
signal: AbortSignal.timeout(Math.min(remaining, POLL_REQUEST_TIMEOUT_MS)),
|
|
97
|
-
}).catch((e) => {
|
|
98
|
-
if (!isAbort(e))
|
|
99
|
-
throw e;
|
|
100
|
-
throw remaining <= POLL_REQUEST_TIMEOUT_MS ? overdue() : new Error(`the platform did not answer a status poll within ${POLL_REQUEST_TIMEOUT_MS / 1000}s — check \`insta status\` or re-run`);
|
|
101
|
-
});
|
|
102
|
-
const state = res.body?.state;
|
|
103
|
-
await watchLogs?.(operationId, state === 'failed' || state === 'live', Math.max(0, deadline - now()));
|
|
104
|
-
// A failed operation is an ANSWER, not a transport error: the poll worked, and the sentence
|
|
105
|
-
// it carries (usually the gateway's own, e.g. "no Dockerfile at ./api") is the one to show.
|
|
106
|
-
if (state === 'failed') {
|
|
107
|
-
// `||` would let a non-string through and the CLI would print "[object Object]" for the one
|
|
108
|
-
// sentence that explains the failure. Only a non-empty string is a message.
|
|
109
|
-
const error = res.body?.error;
|
|
110
|
-
return { failed: typeof error === 'string' && error ? error : 'the deploy failed' };
|
|
88
|
+
const logController = new AbortController();
|
|
89
|
+
const logTask = watchLogs ? (async () => {
|
|
90
|
+
while (!logController.signal.aborted && now() < deadline) {
|
|
91
|
+
await watchLogs(operationId, false, Math.max(0, deadline - now()), logController.signal);
|
|
92
|
+
await delay(Math.min(POLL_MS, Math.max(0, deadline - now())), undefined, { signal: logController.signal });
|
|
111
93
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
94
|
+
})().catch(() => {
|
|
95
|
+
if (!logController.signal.aborted)
|
|
96
|
+
log(`Could not follow build logs. Retry with: insta build-logs ${operationId}`);
|
|
97
|
+
}) : undefined;
|
|
98
|
+
let last = '';
|
|
99
|
+
try {
|
|
100
|
+
for (;;) {
|
|
101
|
+
const remaining = deadline - now();
|
|
102
|
+
if (remaining <= 0)
|
|
103
|
+
throw overdue();
|
|
104
|
+
const res = await api.rawRequest('GET', `/projects/${projectId}/archive-deploys/${encodeURIComponent(operationId)}`, undefined, {
|
|
105
|
+
signal: AbortSignal.timeout(Math.min(remaining, POLL_REQUEST_TIMEOUT_MS)),
|
|
106
|
+
}).catch((e) => {
|
|
107
|
+
if (!isAbort(e))
|
|
108
|
+
throw e;
|
|
109
|
+
throw remaining <= POLL_REQUEST_TIMEOUT_MS ? overdue() : new Error(`the platform did not answer a status poll within ${POLL_REQUEST_TIMEOUT_MS / 1000}s — check \`insta status\` or re-run`);
|
|
110
|
+
});
|
|
111
|
+
const state = res.body?.state;
|
|
112
|
+
if (state === 'failed' || state === 'live') {
|
|
113
|
+
logController.abort();
|
|
114
|
+
await logTask;
|
|
115
|
+
await watchLogs?.(operationId, true, Math.max(0, deadline - now()));
|
|
117
116
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
117
|
+
if (state === 'failed') {
|
|
118
|
+
const error = res.body?.error;
|
|
119
|
+
return { failed: typeof error === 'string' && error ? error : 'the deploy failed' };
|
|
120
|
+
}
|
|
121
|
+
if (state === 'live') {
|
|
122
|
+
const image = res.body?.imageRef;
|
|
123
|
+
const url = res.body?.url;
|
|
124
|
+
if (typeof image !== 'string' || !image || typeof url !== 'string' || !url) {
|
|
125
|
+
throw new Error('the deploy finished but the platform returned no image or URL for it — check `insta status`');
|
|
126
|
+
}
|
|
127
|
+
const optionalString = (field, v) => {
|
|
128
|
+
if (v === undefined || v === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (typeof v !== 'string')
|
|
131
|
+
throw new Error(`the platform returned a non-string ${field} for the deploy — upgrade with \`insta upgrade\``);
|
|
132
|
+
return v;
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
image, url,
|
|
136
|
+
branch: optionalString('branch', res.body.branch) ?? branch,
|
|
137
|
+
group: optionalString('group', res.body.group) ?? opts.group ?? '',
|
|
138
|
+
machineId: optionalString('machineId', res.body.machineId),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (state !== 'queued' && state !== 'building' && state !== 'deploying') {
|
|
142
|
+
throw new Error(`the platform reported an unknown deploy state (${JSON.stringify(state)}) — upgrade with \`insta upgrade\``);
|
|
143
|
+
}
|
|
144
|
+
if (state !== last) {
|
|
145
|
+
log(state === 'deploying' ? 'image built, deploying it' : `${state}…`);
|
|
146
|
+
last = state;
|
|
147
|
+
}
|
|
148
|
+
await wait(POLL_MS);
|
|
143
149
|
}
|
|
144
|
-
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
logController.abort();
|
|
153
|
+
await logTask;
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
//# sourceMappingURL=deploy-archive.js.map
|