neonctl 2.24.2 → 2.25.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.
- package/README.md +137 -47
- package/commands/auth.js +9 -0
- package/commands/bootstrap.js +603 -0
- package/commands/branches.js +6 -4
- package/commands/bucket.js +118 -5
- package/commands/checkout.js +25 -8
- package/commands/config.js +98 -10
- package/commands/deploy.js +2 -1
- package/commands/dev.js +11 -57
- package/commands/env.js +9 -2
- package/commands/functions.js +81 -16
- package/commands/index.js +2 -0
- package/commands/link.js +448 -108
- package/commands/projects.js +2 -2
- package/commands/set_context.js +5 -1
- package/config_format.js +8 -2
- package/context.js +33 -5
- package/dev/env.js +38 -0
- package/dev/functions.js +2 -4
- package/dev/runtime.js +2 -2
- package/index.js +1 -0
- package/package.json +5 -5
- package/storage_api.js +34 -0
- package/utils/bootstrap.js +243 -0
- package/utils/esbuild.js +11 -2
package/commands/functions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isAxiosError } from 'axios';
|
|
4
4
|
import { retryOnLock } from '../api.js';
|
|
@@ -21,8 +21,27 @@ const DEPLOYMENT_FIELDS = [
|
|
|
21
21
|
'memory_mib',
|
|
22
22
|
'created_at',
|
|
23
23
|
];
|
|
24
|
+
// Deploy emits the resolved deployment plus the function's invocation_url, so a
|
|
25
|
+
// successful `functions deploy` tells the user exactly where to call the function.
|
|
26
|
+
const DEPLOY_RESULT_FIELDS = [
|
|
27
|
+
'id',
|
|
28
|
+
'status',
|
|
29
|
+
'invocation_url',
|
|
30
|
+
'runtime',
|
|
31
|
+
'memory_mib',
|
|
32
|
+
'created_at',
|
|
33
|
+
];
|
|
34
|
+
// In table mode a failed build's reason gets its own "deployment error"
|
|
35
|
+
// section after the deployment table; json/yaml carry the raw `error` field.
|
|
36
|
+
const writeDeploymentErrorSection = (out, dep) => {
|
|
37
|
+
if (dep.status === 'failed' && dep.error) {
|
|
38
|
+
out.write({ reason: dep.error }, { fields: ['reason'], title: 'deployment error' });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
24
41
|
const SLUG_PATTERN = /^[a-z0-9]{1,20}$/;
|
|
25
42
|
const SLUG_HELP = 'Use 1-20 lowercase letters and digits (no hyphens or other characters).';
|
|
43
|
+
// Entry-point discovery order inside --src.
|
|
44
|
+
const ENTRY_CANDIDATES = ['index.ts', 'index.mjs', 'index.js'];
|
|
26
45
|
// Overridable so tests can poll fast; defaults to 2s in real use.
|
|
27
46
|
const POLL_INTERVAL_MS = Number(process.env.NEON_FUNCTIONS_POLL_INTERVAL_MS) || 2000;
|
|
28
47
|
// Upper bound on --wait polling so the CLI never hangs (e.g. if our deployment
|
|
@@ -52,13 +71,19 @@ export const builder = (argv) => argv
|
|
|
52
71
|
demandOption: true,
|
|
53
72
|
})
|
|
54
73
|
.options({
|
|
74
|
+
src: {
|
|
75
|
+
describe: 'Function source: a directory containing index.ts, index.mjs, or index.js, or a path to the entry file',
|
|
76
|
+
type: 'string',
|
|
77
|
+
},
|
|
78
|
+
// Removed flags, kept hidden so old invocations fail loudly instead
|
|
79
|
+
// of being silently ignored (the CLI has no .strictOptions()).
|
|
55
80
|
path: {
|
|
56
|
-
describe: 'Base directory for the function (resolves --entry)',
|
|
57
81
|
type: 'string',
|
|
82
|
+
hidden: true,
|
|
58
83
|
},
|
|
59
84
|
entry: {
|
|
60
|
-
describe: 'Entry file to bundle, relative to --path',
|
|
61
85
|
type: 'string',
|
|
86
|
+
hidden: true,
|
|
62
87
|
},
|
|
63
88
|
runtime: {
|
|
64
89
|
describe: 'Function runtime',
|
|
@@ -77,10 +102,19 @@ export const builder = (argv) => argv
|
|
|
77
102
|
},
|
|
78
103
|
}), (args) => deploy(args))
|
|
79
104
|
.command('list', 'List functions on the branch', (yargs) => yargs, (args) => list(args))
|
|
80
|
-
.command('get <slug>', "Show a function's details", (yargs) => yargs
|
|
105
|
+
.command('get <slug>', "Show a function's details", (yargs) => yargs
|
|
106
|
+
.positional('slug', {
|
|
81
107
|
describe: 'Function slug',
|
|
82
108
|
type: 'string',
|
|
83
109
|
demandOption: true,
|
|
110
|
+
})
|
|
111
|
+
.options({
|
|
112
|
+
'list-env-variables': {
|
|
113
|
+
describe: 'List the environment variable names of the active deployment',
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
alias: 'E',
|
|
116
|
+
default: false,
|
|
117
|
+
},
|
|
84
118
|
}), (args) => get(args))
|
|
85
119
|
.command('delete <slug>', 'Delete a function on the branch', (yargs) => yargs.positional('slug', {
|
|
86
120
|
describe: 'Function slug',
|
|
@@ -104,6 +138,15 @@ const parseEnv = (entries) => {
|
|
|
104
138
|
return JSON.stringify(map);
|
|
105
139
|
};
|
|
106
140
|
const statusHint = (slug, projectId, branchId) => `Check status with: neonctl functions get ${slug} --project-id ${projectId} --branch ${branchId}`;
|
|
141
|
+
// Emit the resolved deployment together with the function's invocation_url, so the
|
|
142
|
+
// deploy output shows where the function is reachable (not just the deployment id).
|
|
143
|
+
const emitDeployResult = (props, deployment, fn) => {
|
|
144
|
+
const out = writer(props).write({ ...deployment, invocation_url: fn?.invocation_url }, { fields: DEPLOY_RESULT_FIELDS });
|
|
145
|
+
if (props.output !== 'json' && props.output !== 'yaml') {
|
|
146
|
+
writeDeploymentErrorSection(out, deployment);
|
|
147
|
+
}
|
|
148
|
+
out.end();
|
|
149
|
+
};
|
|
107
150
|
// A poll error worth retrying: a network error (no HTTP response), a 5xx, or a
|
|
108
151
|
// 404 from eventual consistency. Anything else (e.g. 401/403) is surfaced.
|
|
109
152
|
const isTransient = (err) => isAxiosError(err) &&
|
|
@@ -111,27 +154,36 @@ const isTransient = (err) => isAxiosError(err) &&
|
|
|
111
154
|
err.response.status === 404 ||
|
|
112
155
|
err.response.status >= 500);
|
|
113
156
|
const deploy = async (props) => {
|
|
157
|
+
if (props.path !== undefined || props.entry !== undefined) {
|
|
158
|
+
throw new Error('--path and --entry were removed. Use --src <dir>; the entry point ' +
|
|
159
|
+
'is discovered as index.ts, index.mjs, or index.js in that directory.');
|
|
160
|
+
}
|
|
114
161
|
// At least one deploy option must be passed (--wait is excluded: it controls
|
|
115
162
|
// output, not what gets deployed).
|
|
116
|
-
const hasOption = props.
|
|
117
|
-
props.entry !== undefined ||
|
|
163
|
+
const hasOption = props.src !== undefined ||
|
|
118
164
|
props.env !== undefined ||
|
|
119
165
|
props.runtime !== undefined;
|
|
120
166
|
if (!hasOption) {
|
|
121
|
-
throw new Error('Provide at least one option to deploy, e.g. --
|
|
167
|
+
throw new Error('Provide at least one option to deploy, e.g. --src or --env. ' +
|
|
122
168
|
'See: neonctl functions deploy --help.');
|
|
123
169
|
}
|
|
124
170
|
// Cheap, offline validation first - fail before any network round-trip.
|
|
125
171
|
if (!SLUG_PATTERN.test(props.slug)) {
|
|
126
172
|
throw new Error(`Invalid function slug "${props.slug}". ${SLUG_HELP}`);
|
|
127
173
|
}
|
|
128
|
-
const
|
|
129
|
-
const entry = props.entry ?? 'index.ts';
|
|
174
|
+
const src = props.src ?? '.';
|
|
130
175
|
const runtime = props.runtime ?? 'nodejs24';
|
|
131
176
|
const environment = parseEnv(props.env);
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
throw new Error(
|
|
177
|
+
const srcStat = statSync(src, { throwIfNoEntry: false });
|
|
178
|
+
if (srcStat === undefined) {
|
|
179
|
+
throw new Error(`--src path not found: ${src}.`);
|
|
180
|
+
}
|
|
181
|
+
// A file is used as the entry point directly; a directory triggers discovery.
|
|
182
|
+
const source = srcStat.isFile()
|
|
183
|
+
? src
|
|
184
|
+
: ENTRY_CANDIDATES.map((name) => join(src, name)).find((p) => existsSync(p));
|
|
185
|
+
if (source === undefined) {
|
|
186
|
+
throw new Error(`No entry file found in ${src}. Expected one of: ${ENTRY_CANDIDATES.join(', ')}.`);
|
|
135
187
|
}
|
|
136
188
|
// Bundle before any network round-trip so a bundling failure fails fast.
|
|
137
189
|
const zip = zipBundle(await bundleEntry(source));
|
|
@@ -165,6 +217,9 @@ const deploy = async (props) => {
|
|
|
165
217
|
// any version if there was none). --no-wait stops there; --wait stops at a
|
|
166
218
|
// terminal status. Bounded by POLL_TIMEOUT_MS so it never hangs.
|
|
167
219
|
let resolved;
|
|
220
|
+
// The function carries the invocation_url; keep the whole record (not just its
|
|
221
|
+
// active_deployment) so we can surface that URL on success.
|
|
222
|
+
let resolvedFn;
|
|
168
223
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
169
224
|
try {
|
|
170
225
|
while (!interrupted && Date.now() < deadline) {
|
|
@@ -173,18 +228,20 @@ const deploy = async (props) => {
|
|
|
173
228
|
break;
|
|
174
229
|
// The deploy already succeeded server-side; tolerate transient poll
|
|
175
230
|
// failures and retry on the next interval. Surface anything else.
|
|
176
|
-
let
|
|
231
|
+
let fn;
|
|
177
232
|
try {
|
|
178
|
-
|
|
233
|
+
fn = await getFunction(props.apiClient, props.projectId, branchId, props.slug);
|
|
179
234
|
}
|
|
180
235
|
catch (err) {
|
|
181
236
|
if (isTransient(err))
|
|
182
237
|
continue;
|
|
183
238
|
throw err;
|
|
184
239
|
}
|
|
240
|
+
const dep = fn.active_deployment;
|
|
185
241
|
const isNew = dep !== undefined && (before === undefined || dep.id > before);
|
|
186
242
|
if (isNew && dep) {
|
|
187
243
|
resolved = dep;
|
|
244
|
+
resolvedFn = fn;
|
|
188
245
|
if (!props.wait)
|
|
189
246
|
break;
|
|
190
247
|
if (dep.status === 'completed' || dep.status === 'failed')
|
|
@@ -199,14 +256,14 @@ const deploy = async (props) => {
|
|
|
199
256
|
if (interrupted) {
|
|
200
257
|
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
201
258
|
if (resolved)
|
|
202
|
-
|
|
259
|
+
emitDeployResult(props, resolved, resolvedFn);
|
|
203
260
|
return;
|
|
204
261
|
}
|
|
205
262
|
if (resolved === undefined) {
|
|
206
263
|
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
207
264
|
throw new Error(`Timed out waiting for the deployment of ${props.slug} to start. It may still be in progress.`);
|
|
208
265
|
}
|
|
209
|
-
|
|
266
|
+
emitDeployResult(props, resolved, resolvedFn);
|
|
210
267
|
if (!props.wait) {
|
|
211
268
|
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
212
269
|
return;
|
|
@@ -238,6 +295,14 @@ const get = async (props) => {
|
|
|
238
295
|
fields: DEPLOYMENT_FIELDS,
|
|
239
296
|
title: 'active deployment',
|
|
240
297
|
});
|
|
298
|
+
writeDeploymentErrorSection(out, fn.active_deployment);
|
|
299
|
+
}
|
|
300
|
+
if (props.listEnvVariables) {
|
|
301
|
+
out.write((fn.active_deployment?.environment ?? []).map((name) => ({ name })), {
|
|
302
|
+
fields: ['name'],
|
|
303
|
+
title: 'environment',
|
|
304
|
+
emptyMessage: 'No environment variables on the active deployment.',
|
|
305
|
+
});
|
|
241
306
|
}
|
|
242
307
|
out.end();
|
|
243
308
|
};
|
package/commands/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import * as config from './config.js';
|
|
|
22
22
|
import * as deploy from './deploy.js';
|
|
23
23
|
import * as env from './env.js';
|
|
24
24
|
import * as bucket from './bucket.js';
|
|
25
|
+
import * as bootstrap from './bootstrap.js';
|
|
25
26
|
export default [
|
|
26
27
|
auth,
|
|
27
28
|
users,
|
|
@@ -47,4 +48,5 @@ export default [
|
|
|
47
48
|
deploy,
|
|
48
49
|
env,
|
|
49
50
|
bucket,
|
|
51
|
+
bootstrap,
|
|
50
52
|
];
|