neonctl 2.24.1 → 2.24.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/README.md +1 -1
- package/commands/checkout.js +42 -15
- package/commands/config.js +46 -1
- package/commands/dev.js +2 -0
- package/commands/env.js +1 -0
- package/dev/env.js +9 -6
- package/package.json +4 -4
- package/utils/branch_picker.js +16 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ step to pick which branch to pin — the same `+ Create a new branch…` + lis
|
|
|
166
166
|
```bash
|
|
167
167
|
? Which organization would you like to link? › Personal Org (org-abc123)
|
|
168
168
|
? Which project would you like to link? › my-app (polished-snowflake-12345678)
|
|
169
|
-
? Which branch would you like to link? ›
|
|
169
|
+
? Which branch would you like to link? › [default] main (br-main-branch-87654321)
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
**Non-interactive (flags or `--params` JSON)** — for scripts and CI:
|
package/commands/checkout.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createBranch, pickBranchInteractively, } from '../utils/branch_picker.j
|
|
|
7
7
|
import { fillSingleProject } from '../utils/enrichers.js';
|
|
8
8
|
import { looksLikeBranchId } from '../utils/formats.js';
|
|
9
9
|
import { autoPullEnvAfterPin } from './env.js';
|
|
10
|
-
import { applyPolicyOnCreate } from './config.js';
|
|
10
|
+
import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout, } from './config.js';
|
|
11
11
|
import { handler as linkHandler } from './link.js';
|
|
12
12
|
// The positional is optional: omitting it in an interactive terminal opens a
|
|
13
13
|
// branch picker. In non-interactive contexts a missing branch is an error.
|
|
@@ -52,7 +52,7 @@ export const handler = async (props) => {
|
|
|
52
52
|
// (--project-id flag > .neon file > single-project auto-detect); when
|
|
53
53
|
// nothing resolves, fall back to an interactive `neonctl link`.
|
|
54
54
|
const projectId = await resolveProjectId(props);
|
|
55
|
-
const { branchId, created } = await resolveBranchId(props, projectId);
|
|
55
|
+
const { branchId, created, policyApplied } = await resolveBranchId(props, projectId);
|
|
56
56
|
const orgId = await resolveOrgId(props, projectId);
|
|
57
57
|
// `checkout` is a thin helper over `set-context`. It fully "heals" the
|
|
58
58
|
// context file: it always (re)writes `projectId`, `branchId`, and `orgId`
|
|
@@ -64,15 +64,17 @@ export const handler = async (props) => {
|
|
|
64
64
|
branchId,
|
|
65
65
|
});
|
|
66
66
|
log.info('Checked out branch %s on project %s%s. Updated %s.', branchId, projectId, orgId ? ` (org ${orgId})` : '', props.contextFile);
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
67
|
+
// When checkout *created* the branch and a neon.ts exists, the branch was created straight
|
|
68
|
+
// from the policy (evaluated as a new branch) so its settings/infra are already applied —
|
|
69
|
+
// see `policyApplied`. The fallback below covers the case where the branch was created bare
|
|
70
|
+
// (e.g. a policy-driven create wasn't possible); `applyPolicyOnCreate` is a no-op when there
|
|
71
|
+
// is no neon.ts on disk. Checking out an existing branch never reconciles it.
|
|
72
|
+
if (created && !policyApplied) {
|
|
72
73
|
await applyPolicyOnCreate({
|
|
73
74
|
projectId,
|
|
74
75
|
branchId,
|
|
75
76
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
77
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
// Bundle `env pull` so the branch-first loop is just link + checkout: the branch you
|
|
@@ -94,26 +96,27 @@ const resolveBranchId = async (props, projectId) => {
|
|
|
94
96
|
'or run interactively to pick one from a list.',
|
|
95
97
|
});
|
|
96
98
|
if (picked.kind === 'existing') {
|
|
97
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
branchId: picked.branchId,
|
|
101
|
+
created: false,
|
|
102
|
+
policyApplied: false,
|
|
103
|
+
};
|
|
98
104
|
}
|
|
99
105
|
// The user chose "create a new branch" from the picker.
|
|
100
|
-
return
|
|
101
|
-
branchId: await createBranch(props.apiClient, projectId, picked.name, branches),
|
|
102
|
-
created: true,
|
|
103
|
-
};
|
|
106
|
+
return createCheckoutBranch(props, projectId, picked.name, branches);
|
|
104
107
|
}
|
|
105
108
|
const ref = props.id;
|
|
106
109
|
// A `br-…` value is an id; match strictly by id and never offer to create.
|
|
107
110
|
if (looksLikeBranchId(ref)) {
|
|
108
111
|
const byId = branches.find((b) => b.id === ref);
|
|
109
112
|
if (byId) {
|
|
110
|
-
return { branchId: byId.id, created: false };
|
|
113
|
+
return { branchId: byId.id, created: false, policyApplied: false };
|
|
111
114
|
}
|
|
112
115
|
throw new Error(notFoundMessage(ref, branches));
|
|
113
116
|
}
|
|
114
117
|
const byName = branches.find((b) => b.name === ref);
|
|
115
118
|
if (byName) {
|
|
116
|
-
return { branchId: byName.id, created: false };
|
|
119
|
+
return { branchId: byName.id, created: false, policyApplied: false };
|
|
117
120
|
}
|
|
118
121
|
// Name not found: offer to create it interactively, mirroring `branch create`.
|
|
119
122
|
if (isCi() || !process.stdout.isTTY) {
|
|
@@ -129,9 +132,33 @@ const resolveBranchId = async (props, projectId) => {
|
|
|
129
132
|
if (!create) {
|
|
130
133
|
throw new Error(`Aborted: branch "${ref}" was not found and not created.`);
|
|
131
134
|
}
|
|
135
|
+
return createCheckoutBranch(props, projectId, ref, branches);
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Create the branch to check out. When a `neon.ts` exists, route through the policy-driven
|
|
139
|
+
* create so the new branch comes up branched from the policy's `parent` and configured with
|
|
140
|
+
* its declared TTL / compute / services (evaluated as a *new* branch). Otherwise fall back to
|
|
141
|
+
* a bare branch off the default — the handler then applies the policy (a no-op with no
|
|
142
|
+
* `neon.ts`).
|
|
143
|
+
*/
|
|
144
|
+
const createCheckoutBranch = async (props, projectId, name, branches) => {
|
|
145
|
+
const fromPolicy = await createBranchFromPolicyOnCheckout({
|
|
146
|
+
projectId,
|
|
147
|
+
branchName: name,
|
|
148
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
149
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
150
|
+
});
|
|
151
|
+
if (fromPolicy) {
|
|
152
|
+
return {
|
|
153
|
+
branchId: fromPolicy.branchId,
|
|
154
|
+
created: true,
|
|
155
|
+
policyApplied: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
132
158
|
return {
|
|
133
|
-
branchId: await createBranch(props.apiClient, projectId,
|
|
159
|
+
branchId: await createBranch(props.apiClient, projectId, name, branches),
|
|
134
160
|
created: true,
|
|
161
|
+
policyApplied: false,
|
|
135
162
|
};
|
|
136
163
|
};
|
|
137
164
|
const notFoundMessage = (ref, branches) => `Branch ${ref} not found.\nAvailable branches: ${branches
|
package/commands/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveConfig } from '@neondatabase/config';
|
|
2
|
-
import { apply, inspect, loadConfigFromFile, plan, } from '@neondatabase/config-runtime';
|
|
2
|
+
import { apply, createBranch as createBranchFromPolicy, inspect, loadConfigFromFile, plan, } from '@neondatabase/config-runtime';
|
|
3
3
|
import { toNeonConfigView } from '../config_format.js';
|
|
4
4
|
import { log } from '../log.js';
|
|
5
5
|
import { loadEnvFileIntoProcess } from '../env_file.js';
|
|
@@ -107,6 +107,7 @@ export const status = async (props) => {
|
|
|
107
107
|
projectId: props.projectId,
|
|
108
108
|
branchId,
|
|
109
109
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
110
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
110
111
|
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
111
112
|
});
|
|
112
113
|
// The pulled `config` carries the branch's tuning inside a closure that JSON can't
|
|
@@ -141,6 +142,7 @@ export const planCmd = async (props) => {
|
|
|
141
142
|
projectId: props.projectId,
|
|
142
143
|
branchId,
|
|
143
144
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
145
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
144
146
|
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
145
147
|
});
|
|
146
148
|
reportPushResult(props, result, 'plan');
|
|
@@ -152,6 +154,7 @@ export const applyCmd = async (props) => {
|
|
|
152
154
|
projectId: props.projectId,
|
|
153
155
|
branchId,
|
|
154
156
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
157
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
155
158
|
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
156
159
|
...(props.updateExisting ? { updateExisting: true } : {}),
|
|
157
160
|
...(props.allowProtected ? { allowProtectedBranch: true } : {}),
|
|
@@ -235,11 +238,16 @@ export const applyPolicyOnCreate = async (props) => {
|
|
|
235
238
|
projectId: props.projectId,
|
|
236
239
|
branchId: props.branchId,
|
|
237
240
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
241
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
238
242
|
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
239
243
|
updateExisting: true,
|
|
240
244
|
allowProtectedBranch: true,
|
|
241
245
|
bundleFunction: neonctlBundler,
|
|
242
246
|
});
|
|
247
|
+
logPolicyResult(result);
|
|
248
|
+
};
|
|
249
|
+
/** Log a one-line summary of what applying a `neon.ts` policy changed (or that nothing did). */
|
|
250
|
+
const logPolicyResult = (result) => {
|
|
243
251
|
const changes = result.applied.filter((c) => c.action !== 'noop');
|
|
244
252
|
if (changes.length === 0) {
|
|
245
253
|
log.info('neon.ts applied — no changes were needed.');
|
|
@@ -247,3 +255,40 @@ export const applyPolicyOnCreate = async (props) => {
|
|
|
247
255
|
}
|
|
248
256
|
log.info('neon.ts applied — %d change%s: %s', changes.length, changes.length === 1 ? '' : 's', changes.map((c) => `${c.action} ${c.identifier}`).join(', '));
|
|
249
257
|
};
|
|
258
|
+
/**
|
|
259
|
+
* Create a branch **from** the local `neon.ts` policy. Returns `null` when there is no
|
|
260
|
+
* `neon.ts` on the path from cwd up to the repo root, so `neonctl checkout` can fall back to a
|
|
261
|
+
* bare branch create.
|
|
262
|
+
*
|
|
263
|
+
* Unlike a bare create followed by {@link applyPolicyOnCreate}, this evaluates the policy for
|
|
264
|
+
* the **new** branch (`exists: false`): the runtime branches from the policy's `parent` and
|
|
265
|
+
* brings the branch up with its declared TTL / compute settings / services. That's what makes
|
|
266
|
+
* a policy keyed on `!branch.exists` (the common "only configure new branches" shape) take
|
|
267
|
+
* effect on the very first `checkout` — a bare create + `apply` always saw `exists: true` and
|
|
268
|
+
* skipped that block.
|
|
269
|
+
*/
|
|
270
|
+
export const createBranchFromPolicyOnCheckout = async (props) => {
|
|
271
|
+
let config;
|
|
272
|
+
try {
|
|
273
|
+
({ config } = await loadConfigFromFile({
|
|
274
|
+
...(props.cwd ? { cwd: props.cwd } : {}),
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
279
|
+
if (/Could not find a Neon config file/i.test(message))
|
|
280
|
+
return null;
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
const { branchId, branchName, result } = await createBranchFromPolicy(config, {
|
|
284
|
+
projectId: props.projectId,
|
|
285
|
+
branchName: props.branchName,
|
|
286
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
287
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
288
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
289
|
+
bundleFunction: neonctlBundler,
|
|
290
|
+
});
|
|
291
|
+
log.info('Created branch %s (%s) from neon.ts policy.', branchName, branchId);
|
|
292
|
+
logPolicyResult(result);
|
|
293
|
+
return { branchId };
|
|
294
|
+
};
|
package/commands/dev.js
CHANGED
|
@@ -56,6 +56,7 @@ const runSingleSource = async (props) => {
|
|
|
56
56
|
...(props.projectId ? { projectId: props.projectId } : {}),
|
|
57
57
|
...(branchId ? { branchId } : {}),
|
|
58
58
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
59
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
59
60
|
});
|
|
60
61
|
const unit = {
|
|
61
62
|
slug: null,
|
|
@@ -93,6 +94,7 @@ const runFromConfig = async (props) => {
|
|
|
93
94
|
...(props.projectId ? { projectId: props.projectId } : {}),
|
|
94
95
|
...(branchId ? { branchId } : {}),
|
|
95
96
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
97
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
96
98
|
});
|
|
97
99
|
const units = planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
|
|
98
100
|
// Re-derive the units from neon.ts on demand so the config watcher can hot-add/remove
|
package/commands/env.js
CHANGED
|
@@ -49,6 +49,7 @@ export const pull = async (props) => {
|
|
|
49
49
|
projectId: props.projectId,
|
|
50
50
|
branchId,
|
|
51
51
|
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
52
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
52
53
|
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
53
54
|
});
|
|
54
55
|
const neonVars = pickNeonVars(vars);
|
package/dev/env.js
CHANGED
|
@@ -2,6 +2,12 @@ import { loadConfigFromFile, } from '@neondatabase/config';
|
|
|
2
2
|
import { plan, pullConfig, } from '@neondatabase/config-runtime';
|
|
3
3
|
import { fetchEnv, toEntries } from '@neondatabase/env';
|
|
4
4
|
import { log } from '../log.js';
|
|
5
|
+
/** The API-targeting options every runtime call forwards from the context. */
|
|
6
|
+
const apiOptions = (ctx) => ({
|
|
7
|
+
...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
|
|
8
|
+
...(ctx.apiHost ? { apiHost: ctx.apiHost } : {}),
|
|
9
|
+
...(ctx.api ? { api: ctx.api } : {}),
|
|
10
|
+
});
|
|
5
11
|
/**
|
|
6
12
|
* Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
|
|
7
13
|
* Data API, a bucket, the AI Gateway) that the linked remote branch does not
|
|
@@ -70,8 +76,7 @@ export const resolveNeonEnvVars = async (ctx) => {
|
|
|
70
76
|
const pulled = await pullConfig({
|
|
71
77
|
projectId: ctx.projectId,
|
|
72
78
|
branchId: ctx.branchId,
|
|
73
|
-
...(ctx
|
|
74
|
-
...(ctx.api ? { api: ctx.api } : {}),
|
|
79
|
+
...apiOptions(ctx),
|
|
75
80
|
});
|
|
76
81
|
// `pulled.config` is already a `Config` (static auth/dataApi toggles + a branch
|
|
77
82
|
// tuning closure), so it feeds straight into fetchEnv — no wrapping needed.
|
|
@@ -149,8 +154,7 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
|
149
154
|
const result = await plan(config, {
|
|
150
155
|
projectId: ctx.projectId,
|
|
151
156
|
branchId: ctx.branchId,
|
|
152
|
-
...(ctx
|
|
153
|
-
...(ctx.api ? { api: ctx.api } : {}),
|
|
157
|
+
...apiOptions(ctx),
|
|
154
158
|
});
|
|
155
159
|
const missing = result.applied.filter(isMissingResource);
|
|
156
160
|
if (missing.length === 0)
|
|
@@ -174,8 +178,7 @@ const fetchAndProject = async (config, ctx) => {
|
|
|
174
178
|
const env = await fetchEnv(config, {
|
|
175
179
|
projectId: ctx.projectId,
|
|
176
180
|
branchId: ctx.branchId,
|
|
177
|
-
...(ctx
|
|
178
|
-
...(ctx.api ? { api: ctx.api } : {}),
|
|
181
|
+
...apiOptions(ctx),
|
|
179
182
|
});
|
|
180
183
|
return toEntries(env);
|
|
181
184
|
};
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+ssh://git@github.com/neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "2.24.
|
|
8
|
+
"version": "2.24.2",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@hono/node-server": "2.0.4",
|
|
61
61
|
"@neondatabase/api-client": "2.7.1",
|
|
62
|
-
"@neondatabase/config": "0.4.
|
|
63
|
-
"@neondatabase/config-runtime": "0.4.
|
|
64
|
-
"@neondatabase/env": "0.3.
|
|
62
|
+
"@neondatabase/config": "0.4.2",
|
|
63
|
+
"@neondatabase/config-runtime": "0.4.2",
|
|
64
|
+
"@neondatabase/env": "0.3.2",
|
|
65
65
|
"@segment/analytics-node": "1.3.0",
|
|
66
66
|
"axios": "1.7.2",
|
|
67
67
|
"axios-debug-log": "1.0.0",
|
package/utils/branch_picker.js
CHANGED
|
@@ -5,6 +5,21 @@ import { log } from '../log.js';
|
|
|
5
5
|
import { isCi } from '../env.js';
|
|
6
6
|
/** Sentinel `value` for the "create a new branch" choice (no branch id can collide). */
|
|
7
7
|
const CREATE_BRANCH_CHOICE = Symbol('create-branch');
|
|
8
|
+
/**
|
|
9
|
+
* Render a branch's display name with the same word labels as `neonctl branch list`
|
|
10
|
+
* (`[default]`, `[protected]`) instead of symbols, so the picker reads clearly.
|
|
11
|
+
*/
|
|
12
|
+
const branchLabel = (branch) => {
|
|
13
|
+
const labels = [];
|
|
14
|
+
if (branch.default) {
|
|
15
|
+
labels.push('[default]');
|
|
16
|
+
}
|
|
17
|
+
if (branch.protected) {
|
|
18
|
+
labels.push('[protected]');
|
|
19
|
+
}
|
|
20
|
+
labels.push(branch.name);
|
|
21
|
+
return labels.join(' ');
|
|
22
|
+
};
|
|
8
23
|
/**
|
|
9
24
|
* Prompt the user to pick a branch from `branches`, with a "+ Create a new branch…" option
|
|
10
25
|
* pinned to the top (mirroring the project/org pickers). The default selection is the
|
|
@@ -27,7 +42,7 @@ export const pickBranchInteractively = async (branches, opts) => {
|
|
|
27
42
|
choices: [
|
|
28
43
|
{ title: '+ Create a new branch…', value: CREATE_BRANCH_CHOICE },
|
|
29
44
|
...branches.map((b) => ({
|
|
30
|
-
title: `${b
|
|
45
|
+
title: `${branchLabel(b)} (${b.id})`,
|
|
31
46
|
value: b.id,
|
|
32
47
|
})),
|
|
33
48
|
],
|