neonctl 2.23.1 → 2.24.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 +109 -18
- package/commands/auth.js +12 -0
- package/commands/branches.js +14 -6
- package/commands/bucket.js +368 -0
- package/commands/checkout.js +49 -61
- package/commands/config.js +249 -0
- package/commands/deploy.js +25 -0
- package/commands/dev.js +743 -0
- package/commands/env.js +121 -0
- package/commands/index.js +10 -0
- package/commands/link.js +76 -12
- package/config_format.js +66 -0
- package/dev/env.js +199 -0
- package/dev/functions.js +72 -0
- package/dev/inputs.js +63 -0
- package/dev/runtime.js +146 -0
- package/env_file.js +146 -0
- package/index.js +2 -0
- package/package.json +7 -2
- package/pkg.js +23 -1
- package/psql/command/cmd_format.js +12 -12
- package/psql/command/cmd_io.js +2 -2
- package/psql/command/cmd_restrict.js +8 -8
- package/psql/describe/formatters.js +1 -1
- package/psql/print/aligned.js +30 -30
- package/psql/print/json.js +5 -5
- package/psql/scanner/sql.js +4 -4
- package/storage_api.js +114 -0
- package/utils/branch_picker.js +88 -0
- package/utils/esbuild.js +8 -5
- package/utils/zip.js +1 -1
package/commands/checkout.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { EndpointType } from '@neondatabase/api-client';
|
|
2
1
|
import { isAxiosError } from 'axios';
|
|
3
2
|
import prompts from 'prompts';
|
|
4
|
-
import { retryOnLock } from '../api.js';
|
|
5
3
|
import { applyContext, readContextFile } from '../context.js';
|
|
6
4
|
import { isCi } from '../env.js';
|
|
7
5
|
import { log } from '../log.js';
|
|
6
|
+
import { createBranch, pickBranchInteractively, } from '../utils/branch_picker.js';
|
|
8
7
|
import { fillSingleProject } from '../utils/enrichers.js';
|
|
9
8
|
import { looksLikeBranchId } from '../utils/formats.js';
|
|
9
|
+
import { autoPullEnvAfterPin } from './env.js';
|
|
10
|
+
import { applyPolicyOnCreate } from './config.js';
|
|
10
11
|
import { handler as linkHandler } from './link.js';
|
|
11
12
|
// The positional is optional: omitting it in an interactive terminal opens a
|
|
12
13
|
// branch picker. In non-interactive contexts a missing branch is an error.
|
|
@@ -23,6 +24,13 @@ export const builder = (argv) => argv
|
|
|
23
24
|
describe: 'Project ID',
|
|
24
25
|
type: 'string',
|
|
25
26
|
},
|
|
27
|
+
'env-pull': {
|
|
28
|
+
describe: "Pull the branch's Neon env vars (DATABASE_URL, …) into a local .env after " +
|
|
29
|
+
'checkout. On by default; use --no-env-pull to skip (e.g. when injecting env at ' +
|
|
30
|
+
'runtime with `neon-env run` / `neon dev`).',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
26
34
|
})
|
|
27
35
|
.example([
|
|
28
36
|
[
|
|
@@ -44,7 +52,7 @@ export const handler = async (props) => {
|
|
|
44
52
|
// (--project-id flag > .neon file > single-project auto-detect); when
|
|
45
53
|
// nothing resolves, fall back to an interactive `neonctl link`.
|
|
46
54
|
const projectId = await resolveProjectId(props);
|
|
47
|
-
const branchId = await resolveBranchId(props, projectId);
|
|
55
|
+
const { branchId, created } = await resolveBranchId(props, projectId);
|
|
48
56
|
const orgId = await resolveOrgId(props, projectId);
|
|
49
57
|
// `checkout` is a thin helper over `set-context`. It fully "heals" the
|
|
50
58
|
// context file: it always (re)writes `projectId`, `branchId`, and `orgId`
|
|
@@ -56,36 +64,56 @@ export const handler = async (props) => {
|
|
|
56
64
|
branchId,
|
|
57
65
|
});
|
|
58
66
|
log.info('Checked out branch %s on project %s%s. Updated %s.', branchId, projectId, orgId ? ` (org ${orgId})` : '', props.contextFile);
|
|
67
|
+
// Only when checkout just *created* the branch do we apply the local neon.ts policy,
|
|
68
|
+
// so a new branch comes up with the declared settings/infra immediately. Checking out an
|
|
69
|
+
// existing branch never reconciles it — that's an explicit `neonctl deploy` / `config
|
|
70
|
+
// apply`. No neon.ts on disk → nothing to apply.
|
|
71
|
+
if (created) {
|
|
72
|
+
await applyPolicyOnCreate({
|
|
73
|
+
projectId,
|
|
74
|
+
branchId,
|
|
75
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Bundle `env pull` so the branch-first loop is just link + checkout: the branch you
|
|
79
|
+
// checked out is immediately usable for local dev. `--no-env-pull` opts out.
|
|
80
|
+
await autoPullEnvAfterPin({
|
|
81
|
+
...props,
|
|
82
|
+
projectId,
|
|
83
|
+
branch: branchId,
|
|
84
|
+
envPull: props.envPull,
|
|
85
|
+
});
|
|
59
86
|
};
|
|
60
|
-
/**
|
|
61
|
-
* Resolve the branch id to check out.
|
|
62
|
-
*
|
|
63
|
-
* - Branch **id** (`br-…`): looked up by id. A non-existent id is a hard "not
|
|
64
|
-
* found" error — we never offer to create one, since ids are server-assigned.
|
|
65
|
-
* - Branch **name**: looked up by name. If it doesn't exist, in an interactive
|
|
66
|
-
* terminal we offer to create it (like `neonctl branch create --name <name>`);
|
|
67
|
-
* in a non-interactive context it's the usual "not found" error.
|
|
68
|
-
* - **Omitted**: open an interactive picker listing the project's branches (TTY
|
|
69
|
-
* only); in a non-interactive context a missing branch is a hard error.
|
|
70
|
-
*/
|
|
71
87
|
const resolveBranchId = async (props, projectId) => {
|
|
72
88
|
const branches = (await props.apiClient.listProjectBranches({ projectId }))
|
|
73
89
|
.data.branches;
|
|
74
90
|
if (!props.id) {
|
|
75
|
-
|
|
91
|
+
const picked = await pickBranchInteractively(branches, {
|
|
92
|
+
message: 'Which branch would you like to check out?',
|
|
93
|
+
nonInteractiveMessage: 'No branch specified. Pass a branch name or id (e.g. `neonctl checkout main`), ' +
|
|
94
|
+
'or run interactively to pick one from a list.',
|
|
95
|
+
});
|
|
96
|
+
if (picked.kind === 'existing') {
|
|
97
|
+
return { branchId: picked.branchId, created: false };
|
|
98
|
+
}
|
|
99
|
+
// 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
|
+
};
|
|
76
104
|
}
|
|
77
105
|
const ref = props.id;
|
|
78
106
|
// A `br-…` value is an id; match strictly by id and never offer to create.
|
|
79
107
|
if (looksLikeBranchId(ref)) {
|
|
80
108
|
const byId = branches.find((b) => b.id === ref);
|
|
81
109
|
if (byId) {
|
|
82
|
-
return byId.id;
|
|
110
|
+
return { branchId: byId.id, created: false };
|
|
83
111
|
}
|
|
84
112
|
throw new Error(notFoundMessage(ref, branches));
|
|
85
113
|
}
|
|
86
114
|
const byName = branches.find((b) => b.name === ref);
|
|
87
115
|
if (byName) {
|
|
88
|
-
return byName.id;
|
|
116
|
+
return { branchId: byName.id, created: false };
|
|
89
117
|
}
|
|
90
118
|
// Name not found: offer to create it interactively, mirroring `branch create`.
|
|
91
119
|
if (isCi() || !process.stdout.isTTY) {
|
|
@@ -101,54 +129,14 @@ const resolveBranchId = async (props, projectId) => {
|
|
|
101
129
|
if (!create) {
|
|
102
130
|
throw new Error(`Aborted: branch "${ref}" was not found and not created.`);
|
|
103
131
|
}
|
|
104
|
-
return
|
|
132
|
+
return {
|
|
133
|
+
branchId: await createBranch(props.apiClient, projectId, ref, branches),
|
|
134
|
+
created: true,
|
|
135
|
+
};
|
|
105
136
|
};
|
|
106
137
|
const notFoundMessage = (ref, branches) => `Branch ${ref} not found.\nAvailable branches: ${branches
|
|
107
138
|
.map((b) => b.name)
|
|
108
139
|
.join(', ')}`;
|
|
109
|
-
const pickBranchInteractively = async (branches, projectId) => {
|
|
110
|
-
if (isCi() || !process.stdout.isTTY) {
|
|
111
|
-
throw new Error('No branch specified. Pass a branch name or id (e.g. `neonctl checkout main`), ' +
|
|
112
|
-
'or run interactively to pick one from a list.');
|
|
113
|
-
}
|
|
114
|
-
if (branches.length === 0) {
|
|
115
|
-
throw new Error(`No branches found for project ${projectId}.`);
|
|
116
|
-
}
|
|
117
|
-
const defaultIndex = Math.max(0, branches.findIndex((b) => b.default));
|
|
118
|
-
const { branchId } = await prompts({
|
|
119
|
-
type: 'select',
|
|
120
|
-
name: 'branchId',
|
|
121
|
-
message: 'Which branch would you like to check out?',
|
|
122
|
-
choices: branches.map((b) => ({
|
|
123
|
-
title: `${b.default ? '✱ ' : ''}${b.name} (${b.id})`,
|
|
124
|
-
value: b.id,
|
|
125
|
-
})),
|
|
126
|
-
initial: defaultIndex,
|
|
127
|
-
});
|
|
128
|
-
if (!branchId) {
|
|
129
|
-
throw new Error('Aborted: no branch selected.');
|
|
130
|
-
}
|
|
131
|
-
return branchId;
|
|
132
|
-
};
|
|
133
|
-
/**
|
|
134
|
-
* Create a branch with the same defaults as `neonctl branch create --name <name>`:
|
|
135
|
-
* branched from the project's default branch with a read-write compute endpoint.
|
|
136
|
-
*/
|
|
137
|
-
const createBranch = async (props, projectId, name, branches) => {
|
|
138
|
-
const defaultBranch = branches.find((b) => b.default);
|
|
139
|
-
if (!defaultBranch) {
|
|
140
|
-
throw new Error('No default branch found');
|
|
141
|
-
}
|
|
142
|
-
const { data } = await retryOnLock(() => props.apiClient.createProjectBranch(projectId, {
|
|
143
|
-
branch: { name, parent_id: defaultBranch.id },
|
|
144
|
-
endpoints: [{ type: EndpointType.ReadWrite }],
|
|
145
|
-
}));
|
|
146
|
-
if (defaultBranch.protected) {
|
|
147
|
-
log.warning('The parent branch is protected; a unique role password has been generated for the new branch.');
|
|
148
|
-
}
|
|
149
|
-
log.info('Created branch %s (%s).', data.branch.name, data.branch.id);
|
|
150
|
-
return data.branch.id;
|
|
151
|
-
};
|
|
152
140
|
/**
|
|
153
141
|
* Resolve the org id to heal into the context file.
|
|
154
142
|
*
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { resolveConfig } from '@neondatabase/config';
|
|
2
|
+
import { apply, inspect, loadConfigFromFile, plan, } from '@neondatabase/config-runtime';
|
|
3
|
+
import { toNeonConfigView } from '../config_format.js';
|
|
4
|
+
import { log } from '../log.js';
|
|
5
|
+
import { loadEnvFileIntoProcess } from '../env_file.js';
|
|
6
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
7
|
+
import { bundleEntry } from '../utils/esbuild.js';
|
|
8
|
+
import { zipBundle } from '../utils/zip.js';
|
|
9
|
+
import { writer } from '../writer.js';
|
|
10
|
+
/**
|
|
11
|
+
* Bundle a function with neonctl's OWN bundler (the shared esbuild helper) so the
|
|
12
|
+
* config-runtime never has to import esbuild itself. Injecting this keeps esbuild
|
|
13
|
+
* out of config-runtime's static module graph — and therefore out of the packaged
|
|
14
|
+
* neonctl snapshot, which resolves esbuild dynamically at deploy time.
|
|
15
|
+
*/
|
|
16
|
+
const neonctlBundler = async (fn) => zipBundle(await bundleEntry(fn.source));
|
|
17
|
+
const INSPECT_FIELDS = ['project', 'branch', 'config'];
|
|
18
|
+
const APPLIED_FIELDS = ['action', 'kind', 'identifier', 'details'];
|
|
19
|
+
const CONFLICT_FIELDS = [
|
|
20
|
+
'identifier',
|
|
21
|
+
'field',
|
|
22
|
+
'current',
|
|
23
|
+
'desired',
|
|
24
|
+
'reason',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Shared `--env` flag for `config plan|apply` and `deploy`. Loads a `.env` into
|
|
28
|
+
* `process.env` before the policy is evaluated.
|
|
29
|
+
*/
|
|
30
|
+
export const envFlag = {
|
|
31
|
+
env: {
|
|
32
|
+
describe: 'Path to a .env file to load into the environment before evaluating neon.ts ' +
|
|
33
|
+
'(so function env values resolve from it). Existing env vars are not overridden.',
|
|
34
|
+
type: 'string',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/** Apply-only flags, exported so `deploy` can reuse the exact same surface. */
|
|
38
|
+
export const applyFlags = {
|
|
39
|
+
'update-existing': {
|
|
40
|
+
describe: 'Auto-confirm overriding existing remote settings on the branch',
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
44
|
+
'allow-protected': {
|
|
45
|
+
describe: 'Auto-confirm applying to a branch marked protected on Neon',
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export const command = 'config';
|
|
51
|
+
export const describe = 'Manage a branch with a neon.ts policy';
|
|
52
|
+
export const builder = (argv) => argv
|
|
53
|
+
.usage('$0 config <sub-command> [options]')
|
|
54
|
+
.options({
|
|
55
|
+
'project-id': {
|
|
56
|
+
describe: 'Project ID',
|
|
57
|
+
type: 'string',
|
|
58
|
+
},
|
|
59
|
+
branch: {
|
|
60
|
+
describe: 'Branch ID or name',
|
|
61
|
+
type: 'string',
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
.middleware(fillSingleProject)
|
|
65
|
+
.command('status', "Show the branch's live Neon state", (yargs) => yargs.options({
|
|
66
|
+
'config-json': {
|
|
67
|
+
describe: "Print only the branch's live config as neon.ts-shaped JSON " +
|
|
68
|
+
'(services + branch tuning + preview), to stdout. Useful for ' +
|
|
69
|
+
'scripting or copying into a neon.ts.',
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
default: false,
|
|
72
|
+
},
|
|
73
|
+
}), (args) => status(args))
|
|
74
|
+
.command('plan', 'Show what `config apply` would change (dry run)', (yargs) => yargs.options({
|
|
75
|
+
config: {
|
|
76
|
+
describe: 'Path to a neon.ts policy (defaults to walking up from cwd)',
|
|
77
|
+
type: 'string',
|
|
78
|
+
},
|
|
79
|
+
...envFlag,
|
|
80
|
+
}), (args) => planCmd(args))
|
|
81
|
+
.command('apply', 'Apply a neon.ts policy to the branch', (yargs) => yargs.options({
|
|
82
|
+
config: {
|
|
83
|
+
describe: 'Path to a neon.ts policy (defaults to walking up from cwd)',
|
|
84
|
+
type: 'string',
|
|
85
|
+
},
|
|
86
|
+
...envFlag,
|
|
87
|
+
...applyFlags,
|
|
88
|
+
}), (args) => applyCmd(args));
|
|
89
|
+
export const handler = (args) => {
|
|
90
|
+
return args;
|
|
91
|
+
};
|
|
92
|
+
const loadConfig = async (props) => {
|
|
93
|
+
// Load the optional --env file FIRST so a `neon.ts` whose function `env` values read
|
|
94
|
+
// `process.env.X` sees them. Must happen before the policy module is imported/evaluated.
|
|
95
|
+
if (props.env) {
|
|
96
|
+
const applied = loadEnvFileIntoProcess(props.env);
|
|
97
|
+
log.debug('Loaded %d var(s) from %s into the environment: %s', applied.length, props.env, applied.join(', '));
|
|
98
|
+
}
|
|
99
|
+
const { config } = await loadConfigFromFile({
|
|
100
|
+
...(props.config ? { path: props.config } : {}),
|
|
101
|
+
});
|
|
102
|
+
return config;
|
|
103
|
+
};
|
|
104
|
+
export const status = async (props) => {
|
|
105
|
+
const branchId = await branchIdFromProps(props);
|
|
106
|
+
const live = await inspect({
|
|
107
|
+
projectId: props.projectId,
|
|
108
|
+
branchId,
|
|
109
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
110
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
111
|
+
});
|
|
112
|
+
// The pulled `config` carries the branch's tuning inside a closure that JSON can't
|
|
113
|
+
// render. Resolve it against the live branch target to get the concrete settings, then
|
|
114
|
+
// project both that and the separately-pulled preview state into a neon.ts-shaped view.
|
|
115
|
+
const resolved = resolveConfig(live.config, {
|
|
116
|
+
name: live.branch.name,
|
|
117
|
+
id: live.branch.id,
|
|
118
|
+
exists: true,
|
|
119
|
+
isDefault: live.branch.isDefault,
|
|
120
|
+
isProtected: live.branch.protected,
|
|
121
|
+
...(live.branch.parent ? { parentId: live.branch.parent } : {}),
|
|
122
|
+
...(live.branch.expiresAt ? { expiresAt: live.branch.expiresAt } : {}),
|
|
123
|
+
});
|
|
124
|
+
const configView = toNeonConfigView(resolved, live.preview);
|
|
125
|
+
// `--config-json`: emit just the neon.ts-shaped config to stdout (script-friendly,
|
|
126
|
+
// copy-paste-able), regardless of the global --output.
|
|
127
|
+
if (props.configJson) {
|
|
128
|
+
process.stdout.write(`${JSON.stringify(configView, null, 2)}\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Default: the live project/branch tables, but with the unhelpful raw `config` replaced
|
|
132
|
+
// by the resolved neon.ts-shaped view so the user sees enabled infra + branch tuning.
|
|
133
|
+
writer(props).end({ project: live.project, branch: live.branch, config: configView }, { fields: INSPECT_FIELDS });
|
|
134
|
+
};
|
|
135
|
+
export const planCmd = async (props) => {
|
|
136
|
+
const config = await loadConfig(props);
|
|
137
|
+
const branchId = await branchIdFromProps(props);
|
|
138
|
+
// `plan` is a dry run that never bundles, so its options don't accept (or need)
|
|
139
|
+
// an injected bundler — only `apply` does (it uses neonctlBundler).
|
|
140
|
+
const result = await plan(config, {
|
|
141
|
+
projectId: props.projectId,
|
|
142
|
+
branchId,
|
|
143
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
144
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
145
|
+
});
|
|
146
|
+
reportPushResult(props, result, 'plan');
|
|
147
|
+
};
|
|
148
|
+
export const applyCmd = async (props) => {
|
|
149
|
+
const config = await loadConfig(props);
|
|
150
|
+
const branchId = await branchIdFromProps(props);
|
|
151
|
+
const result = await apply(config, {
|
|
152
|
+
projectId: props.projectId,
|
|
153
|
+
branchId,
|
|
154
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
155
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
156
|
+
...(props.updateExisting ? { updateExisting: true } : {}),
|
|
157
|
+
...(props.allowProtected ? { allowProtectedBranch: true } : {}),
|
|
158
|
+
bundleFunction: neonctlBundler,
|
|
159
|
+
});
|
|
160
|
+
reportPushResult(props, result, 'apply');
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Render a {@link PushResult}. JSON/YAML output emits the raw result verbatim so it
|
|
164
|
+
* can be piped; the human-readable path renders the actual changes (dropping noops)
|
|
165
|
+
* and any blocking conflicts as tables, or a "nothing to do" line when both are empty.
|
|
166
|
+
*/
|
|
167
|
+
const reportPushResult = (props, result, mode) => {
|
|
168
|
+
if (props.output === 'json' || props.output === 'yaml') {
|
|
169
|
+
writer(props).end(result, { fields: [] });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const changes = result.applied
|
|
173
|
+
.filter((change) => change.action !== 'noop')
|
|
174
|
+
.map((change) => ({
|
|
175
|
+
action: change.action,
|
|
176
|
+
kind: change.kind,
|
|
177
|
+
identifier: change.identifier,
|
|
178
|
+
details: change.details ? JSON.stringify(change.details) : '',
|
|
179
|
+
}));
|
|
180
|
+
const conflicts = result.conflicts.map((conflict) => ({
|
|
181
|
+
identifier: conflict.identifier,
|
|
182
|
+
field: conflict.field,
|
|
183
|
+
current: stringify(conflict.current),
|
|
184
|
+
desired: stringify(conflict.desired),
|
|
185
|
+
reason: conflict.reason,
|
|
186
|
+
}));
|
|
187
|
+
if (changes.length === 0 && conflicts.length === 0) {
|
|
188
|
+
log.info(`No changes — branch ${result.branchName} already matches the policy.`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const out = writer(props);
|
|
192
|
+
if (changes.length > 0) {
|
|
193
|
+
out.write(changes, {
|
|
194
|
+
fields: APPLIED_FIELDS,
|
|
195
|
+
title: mode === 'plan' ? 'Planned changes' : 'Applied changes',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (conflicts.length > 0) {
|
|
199
|
+
out.write(conflicts, { fields: CONFLICT_FIELDS, title: 'Conflicts' });
|
|
200
|
+
}
|
|
201
|
+
out.end();
|
|
202
|
+
if (conflicts.length > 0) {
|
|
203
|
+
log.info('Resolve the conflicts above, or re-run with --update-existing to override the current remote settings.');
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const stringify = (value) => value === undefined
|
|
207
|
+
? ''
|
|
208
|
+
: typeof value === 'string'
|
|
209
|
+
? value
|
|
210
|
+
: JSON.stringify(value);
|
|
211
|
+
/**
|
|
212
|
+
* Apply a `neon.ts` policy to a **freshly created** branch (used by `neonctl checkout`
|
|
213
|
+
* when it creates a branch). No-op when there is no `neon.ts` on the path from cwd up to
|
|
214
|
+
* the repo root — checkout still succeeds, it just has no policy to apply.
|
|
215
|
+
*
|
|
216
|
+
* The branch was just created by us, so we apply non-interactively (`updateExisting` /
|
|
217
|
+
* `allowProtectedBranch`) — there is no pre-existing state a user would be surprised to
|
|
218
|
+
* see overridden. Functions are bundled with neonctl's own esbuild helper.
|
|
219
|
+
*/
|
|
220
|
+
export const applyPolicyOnCreate = async (props) => {
|
|
221
|
+
let config;
|
|
222
|
+
try {
|
|
223
|
+
({ config } = await loadConfigFromFile({
|
|
224
|
+
...(props.cwd ? { cwd: props.cwd } : {}),
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
229
|
+
if (/Could not find a Neon config file/i.test(message))
|
|
230
|
+
return;
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
log.info('Applying neon.ts policy to the new branch…');
|
|
234
|
+
const result = await apply(config, {
|
|
235
|
+
projectId: props.projectId,
|
|
236
|
+
branchId: props.branchId,
|
|
237
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
238
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
239
|
+
updateExisting: true,
|
|
240
|
+
allowProtectedBranch: true,
|
|
241
|
+
bundleFunction: neonctlBundler,
|
|
242
|
+
});
|
|
243
|
+
const changes = result.applied.filter((c) => c.action !== 'noop');
|
|
244
|
+
if (changes.length === 0) {
|
|
245
|
+
log.info('neon.ts applied — no changes were needed.');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
log.info('neon.ts applied — %d change%s: %s', changes.length, changes.length === 1 ? '' : 's', changes.map((c) => `${c.action} ${c.identifier}`).join(', '));
|
|
249
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fillSingleProject } from '../utils/enrichers.js';
|
|
2
|
+
import { applyCmd, applyFlags, envFlag } from './config.js';
|
|
3
|
+
export const command = 'deploy';
|
|
4
|
+
export const describe = 'Apply a neon.ts policy to a branch (alias for `config apply`)';
|
|
5
|
+
export const builder = (argv) => argv
|
|
6
|
+
.usage('$0 deploy [options]')
|
|
7
|
+
.options({
|
|
8
|
+
'project-id': {
|
|
9
|
+
describe: 'Project ID',
|
|
10
|
+
type: 'string',
|
|
11
|
+
},
|
|
12
|
+
branch: {
|
|
13
|
+
describe: 'Branch ID or name',
|
|
14
|
+
type: 'string',
|
|
15
|
+
},
|
|
16
|
+
config: {
|
|
17
|
+
describe: 'Path to a neon.ts policy (defaults to walking up from cwd)',
|
|
18
|
+
type: 'string',
|
|
19
|
+
},
|
|
20
|
+
...envFlag,
|
|
21
|
+
...applyFlags,
|
|
22
|
+
})
|
|
23
|
+
.middleware(fillSingleProject)
|
|
24
|
+
.strict();
|
|
25
|
+
export const handler = (props) => applyCmd(props);
|