runcloud 0.1.101 → 0.1.102
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/commands/deploy.js +129 -100
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -1,125 +1,154 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { build } from 'esbuild';
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
5
3
|
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
6
|
-
import { requireCredentials,
|
|
7
|
-
import {
|
|
8
|
-
|
|
4
|
+
import { requireCredentials, resolveProject } from '../config.js';
|
|
5
|
+
import { createSnapshot } from '../archive.js';
|
|
6
|
+
import { parseSseFrames } from '../sse.js';
|
|
7
|
+
import { assertKnownServices, parseServiceFilter, readBackendServices } from '../services-manifest.js';
|
|
8
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
9
9
|
const POLL_TIMEOUT_MS = 20 * 60 * 1000;
|
|
10
|
-
function
|
|
11
|
-
|
|
10
|
+
export async function pollRun(client, runId, opts = {}) {
|
|
11
|
+
const intervalMs = opts.intervalMs ?? POLL_INTERVAL_MS;
|
|
12
|
+
const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
|
|
13
|
+
const deadline = Date.now() + timeoutMs;
|
|
14
|
+
for (;;) {
|
|
15
|
+
const run = await client.get(`/newly_backend/deploy_runs/${runId}`);
|
|
16
|
+
if (run.status !== 'running')
|
|
17
|
+
return run;
|
|
18
|
+
if (Date.now() > deadline)
|
|
19
|
+
throw new Error(`Deploy run ${runId} timed out after ${timeoutMs / 1000}s`);
|
|
20
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
21
|
+
}
|
|
12
22
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
export async function streamRun(client, runId, print = (s) => process.stdout.write(s)) {
|
|
24
|
+
let buffer = '';
|
|
25
|
+
let started = false;
|
|
26
|
+
try {
|
|
27
|
+
for await (const chunk of client.stream(`/newly_backend/deploy_runs/${runId}/logs/stream`)) {
|
|
28
|
+
started = true;
|
|
29
|
+
buffer = (buffer + chunk).replace(/\r\n/g, '\n');
|
|
30
|
+
const lastSep = buffer.lastIndexOf('\n\n');
|
|
31
|
+
if (lastSep === -1)
|
|
32
|
+
continue;
|
|
33
|
+
const ready = buffer.slice(0, lastSep + 2);
|
|
34
|
+
buffer = buffer.slice(lastSep + 2);
|
|
35
|
+
for (const frame of parseSseFrames(ready)) {
|
|
36
|
+
if (frame.event === 'done') {
|
|
37
|
+
return JSON.parse(frame.data);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
print(JSON.parse(frame.data));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
print(frame.data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
22
46
|
}
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
50
|
+
return pollRun(client, runId);
|
|
51
|
+
}
|
|
52
|
+
if (started) {
|
|
53
|
+
return pollRun(client, runId);
|
|
54
|
+
}
|
|
55
|
+
throw e;
|
|
31
56
|
}
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
export async function bundleService(dir, service) {
|
|
35
|
-
const result = await build({
|
|
36
|
-
entryPoints: [serviceEntry(dir, service)],
|
|
37
|
-
bundle: true,
|
|
38
|
-
write: false,
|
|
39
|
-
format: 'esm',
|
|
40
|
-
platform: 'browser',
|
|
41
|
-
target: 'es2022',
|
|
42
|
-
conditions: ['workerd', 'worker', 'browser'],
|
|
43
|
-
sourcemap: false,
|
|
44
|
-
legalComments: 'none',
|
|
45
|
-
});
|
|
46
|
-
const output = result.outputFiles[0];
|
|
47
|
-
if (!output)
|
|
48
|
-
throw new Error(`Bundling service ${service.name} produced no output`);
|
|
49
|
-
return Buffer.from(output.contents);
|
|
57
|
+
return pollRun(client, runId);
|
|
50
58
|
}
|
|
51
|
-
export
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const deployment = (await client.get(`/service-deployments/${deploymentId}`));
|
|
56
|
-
if (['ready', 'failed', 'superseded'].includes(deployment.status))
|
|
57
|
-
return deployment;
|
|
58
|
-
if (Date.now() > deadline)
|
|
59
|
-
throw new Error(`Deployment ${deploymentId} timed out`);
|
|
60
|
-
await new Promise((done) => setTimeout(done, intervalMs));
|
|
59
|
+
export function finishOrThrow(run, label) {
|
|
60
|
+
if (run.status === 'succeeded') {
|
|
61
|
+
console.log(`${label} deploy succeeded.`);
|
|
62
|
+
return;
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log(`Building ${service.name}…`);
|
|
65
|
-
const bundle = await bundleService(dir, service);
|
|
66
|
-
const sourceDigest = sha256(bundle);
|
|
67
|
-
const configurationSpec = {
|
|
68
|
-
name: service.name,
|
|
69
|
-
type: service.type,
|
|
70
|
-
public: service.public ?? true,
|
|
71
|
-
bindings: service.bindings ?? {},
|
|
72
|
-
};
|
|
73
|
-
const deployment = (await client.postWithHeaders(`/environment-services/${serviceId}/deployments`, { bundleBase64: bundle.toString('base64'), sourceDigest, configurationSpec }, { 'Idempotency-Key': randomUUID() }));
|
|
74
|
-
const finished = await pollDeployment(client, deployment.id);
|
|
75
|
-
if (finished.status !== 'ready') {
|
|
76
|
-
throw new Error(`Deployment ${finished.id} finished with status ${finished.status}${finished.lastError ? `: ${finished.lastError}` : ''}`);
|
|
64
|
+
if (run.failure_reason === 'no_backend_code') {
|
|
65
|
+
throw new Error('No backend code to deploy (backend/ missing).');
|
|
77
66
|
}
|
|
78
|
-
|
|
79
|
-
return finished;
|
|
67
|
+
throw new Error(`${label} deploy failed (${run.failure_reason ?? 'unknown'}):\n${run.output_tail ?? ''}`);
|
|
80
68
|
}
|
|
81
|
-
export async function
|
|
69
|
+
export async function runDevDeploy(projectId, client, dir, services = []) {
|
|
82
70
|
if (!existsSync(join(dir, 'backend'))) {
|
|
83
|
-
throw new Error('No backend/ in this directory. Run `newly pull <project-id>` first,
|
|
71
|
+
throw new Error('No backend/ in this directory. Run `newly pull <project-id>` first, ' +
|
|
72
|
+
'or cd into a pulled project.');
|
|
84
73
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
74
|
+
console.log('Packaging local backend…');
|
|
75
|
+
const snapshot = await createSnapshot(dir);
|
|
76
|
+
const kib = snapshot.length < 1024 ? '<1' : Math.round(snapshot.length / 1024).toString();
|
|
77
|
+
console.log(`Uploading snapshot (${kib} KiB) and deploying server-side…`);
|
|
78
|
+
const params = new URLSearchParams({ env: 'dev' });
|
|
79
|
+
for (const service of services)
|
|
80
|
+
params.append('services', service);
|
|
81
|
+
const { run_id } = await client.upload(`/newly_backend/deploy/${projectId}?${params}`, snapshot, 'application/gzip');
|
|
82
|
+
const run = await streamRun(client, run_id);
|
|
83
|
+
finishOrThrow(run, 'dev');
|
|
84
|
+
}
|
|
85
|
+
export async function runProdDeploy(projectId, client, gitSha = 'HEAD', services = []) {
|
|
86
|
+
const label = gitSha === 'HEAD' ? 'repo HEAD' : gitSha;
|
|
87
|
+
console.log(`Deploying prod from the canonical project code (${label})…`);
|
|
88
|
+
const { run_id } = await client.post(`/newly_backend/deploy/${projectId}`, {
|
|
89
|
+
env: 'prod',
|
|
90
|
+
git_sha: gitSha,
|
|
91
|
+
services,
|
|
92
|
+
});
|
|
93
|
+
const run = await streamRun(client, run_id);
|
|
94
|
+
finishOrThrow(run, 'prod');
|
|
95
|
+
}
|
|
96
|
+
export async function ensureBackendProvisioned(client, projectId, env, opts = {}) {
|
|
97
|
+
const print = opts.print ?? console.log;
|
|
98
|
+
const status = await client.get(`/newly_backend/status/${projectId}`);
|
|
99
|
+
if (status[env])
|
|
100
|
+
return;
|
|
101
|
+
print(`Provisioning ${env} backend…`);
|
|
102
|
+
if (env === 'dev') {
|
|
103
|
+
await client.post(`/newly_backend/provision_dev/${projectId}`, {
|
|
104
|
+
dev_region: opts.region ?? 'auto',
|
|
105
|
+
});
|
|
99
106
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
deployments.push(await deployService(client, dir, service, serviceIds.get(service.name)));
|
|
107
|
+
else {
|
|
108
|
+
await client.post(`/newly_backend/provision_prod/${projectId}`);
|
|
103
109
|
}
|
|
104
|
-
|
|
110
|
+
print(`Provisioned ${env} backend.`);
|
|
105
111
|
}
|
|
106
112
|
export function registerDeploy(program) {
|
|
107
113
|
program
|
|
108
114
|
.command('deploy')
|
|
109
|
-
.description('
|
|
110
|
-
.
|
|
115
|
+
.description('Provision if needed, then deploy the backend server-side')
|
|
116
|
+
.option('--env <env>', 'dev or prod', 'dev')
|
|
117
|
+
.option('--region <region>', "Newly server region hint for first-time dev provisioning (defaults to 'auto')", 'auto')
|
|
111
118
|
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
112
|
-
.
|
|
119
|
+
.option('--service <names>', 'comma-separated service names from backend/newly.services.json')
|
|
120
|
+
.option('--git-sha <sha>', 'prod git revision to deploy from canonical project code', 'HEAD')
|
|
121
|
+
.option('--yes', 'skip the prod confirmation prompt', false)
|
|
122
|
+
.action(async (opts) => {
|
|
113
123
|
try {
|
|
114
|
-
|
|
124
|
+
if (opts.env !== 'dev' && opts.env !== 'prod')
|
|
125
|
+
throw new Error('--env must be dev or prod');
|
|
115
126
|
const creds = requireCredentials();
|
|
116
|
-
const projectId = resolveProject(
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const projectId = resolveProject(process.cwd(), opts.project);
|
|
128
|
+
const client = new ApiClient(creds.apiUrl, creds.token);
|
|
129
|
+
const services = parseServiceFilter(opts.service);
|
|
130
|
+
assertKnownServices(readBackendServices(process.cwd()), services);
|
|
131
|
+
const stackInfo = await client.get(`/newly_backend/stack/${projectId}`);
|
|
132
|
+
if (!stackInfo.cli_deployable) {
|
|
133
|
+
throw new Error('This project does not support CLI deploys yet. Use the Newly web app to deploy it.');
|
|
134
|
+
}
|
|
135
|
+
if (opts.env === 'dev' && !existsSync(join(process.cwd(), 'backend'))) {
|
|
136
|
+
throw new Error('No backend/ in this directory. Run `newly pull <project-id>` first, ' +
|
|
137
|
+
'or cd into a pulled project.');
|
|
138
|
+
}
|
|
139
|
+
await ensureBackendProvisioned(client, projectId, opts.env, {
|
|
140
|
+
region: opts.region,
|
|
141
|
+
print: console.log,
|
|
142
|
+
});
|
|
143
|
+
if (opts.env === 'prod') {
|
|
144
|
+
await runProdDeploy(projectId, client, opts.gitSha, services);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
await runDevDeploy(projectId, client, process.cwd(), services);
|
|
148
|
+
}
|
|
120
149
|
}
|
|
121
|
-
catch (
|
|
122
|
-
console.error(
|
|
150
|
+
catch (e) {
|
|
151
|
+
console.error(e instanceof ApiError ? friendlyApiError(e) : String(e instanceof Error ? e.message : e));
|
|
123
152
|
process.exitCode = 1;
|
|
124
153
|
}
|
|
125
154
|
});
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.1.
|
|
1
|
+
export const CLI_VERSION = '0.1.102';
|