@gambi97/keel-cli 0.1.1 → 0.2.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 +107 -48
- package/dist/bootstrap/github.js +97 -37
- package/dist/bootstrap/infisical.js +70 -21
- package/dist/bootstrap/scaleway.js +102 -17
- package/dist/config.js +124 -14
- package/dist/contracts.js +46 -0
- package/dist/generate.js +60 -4
- package/dist/index.js +239 -128
- package/dist/prompts.js +270 -63
- package/dist/ui.js +18 -6
- package/package.json +1 -1
- package/templates/.github/scripts/sync-secrets.sh +65 -0
- package/templates/.github/workflows/terraform-drift.yml +19 -1
- package/templates/.github/workflows/terraform-plan.yml +19 -1
- package/templates/README.md +56 -42
- package/templates/_partials/apply-header.yml +33 -0
- package/templates/_partials/apply-job.yml +34 -0
- package/templates/env.tfvars +8 -0
- package/templates/main.tf +12 -1
- package/templates/modules/app_stack/main.tf +34 -0
- package/templates/modules/app_stack/outputs.tf +18 -0
- package/templates/modules/app_stack/variables.tf +5 -0
- package/templates/outputs.tf +22 -0
- package/templates/variables.tf +8 -2
- package/templates/.github/scripts/sync-database-url.sh +0 -49
- package/templates/.github/workflows/terraform-apply.yml +0 -103
- package/templates/prod.tfvars +0 -7
- package/templates/staging.tfvars +0 -7
package/dist/index.js
CHANGED
|
@@ -3,80 +3,137 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { ConfigError, finalizeAnswers, fromEnv, mergeAnswers, missingRequired, } from './config.js';
|
|
6
|
+
import { ConfigError, finalizeAnswers, fromEnv, mergeAnswers, missingRequired, parseEnvironments, } from './config.js';
|
|
7
|
+
import { CI_SECRET_NAMES, CI_VARIABLE_NAMES } from './contracts.js';
|
|
7
8
|
import { generateProject, GenerateError } from './generate.js';
|
|
8
9
|
import { confirmSummary, fillMissing } from './prompts.js';
|
|
9
|
-
import { isDone, loadState, markDone, STATE_FILE, stepData } from './state.js';
|
|
10
|
+
import { isDone, loadState, markDone, STATE_FILE, stepData, } from './state.js';
|
|
10
11
|
import { cancel, intro, log, outro, renderNextSteps, renderSummary, withSpinner } from './ui.js';
|
|
11
12
|
import { ensureStateBucket, validateScalewayCredentials } from './bootstrap/scaleway.js';
|
|
12
|
-
import { bootstrapInfisical,
|
|
13
|
-
import { configureRepo, createContext, ensureRepo, pushRepo, } from './bootstrap/github.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
13
|
+
import { bootstrapInfisical, validateInfisical } from './bootstrap/infisical.js';
|
|
14
|
+
import { assertRepoUsable, configureRepo, createContext, ensureRepo, inspectRepo, pushRepo, } from './bootstrap/github.js';
|
|
15
|
+
/**
|
|
16
|
+
* Single source of truth for the CLI surface: parseArgs consumes these specs
|
|
17
|
+
* and --help is generated from CLI_HELP, whose keys the compiler checks
|
|
18
|
+
* against this table — a flag cannot be added without help text, or vice
|
|
19
|
+
* versa. (The README's CLI reference is the one remaining manual copy.)
|
|
20
|
+
*/
|
|
21
|
+
const CLI_OPTIONS = {
|
|
22
|
+
name: { type: 'string' },
|
|
23
|
+
dir: { type: 'string' },
|
|
24
|
+
region: { type: 'string' },
|
|
25
|
+
'scw-access-key': { type: 'string' },
|
|
26
|
+
'scw-secret-key': { type: 'string' },
|
|
27
|
+
'scw-project-id': { type: 'string' },
|
|
28
|
+
'scw-organization-id': { type: 'string' },
|
|
29
|
+
'infisical-host': { type: 'string' },
|
|
30
|
+
'infisical-client-id': { type: 'string' },
|
|
31
|
+
'infisical-client-secret': { type: 'string' },
|
|
32
|
+
'infisical-project-id': { type: 'string' },
|
|
33
|
+
'infisical-project-name': { type: 'string' },
|
|
34
|
+
'github-token': { type: 'string' },
|
|
35
|
+
'repo-name': { type: 'string' },
|
|
36
|
+
private: { type: 'boolean' },
|
|
37
|
+
public: { type: 'boolean' },
|
|
38
|
+
environments: { type: 'string' },
|
|
39
|
+
'basic-auth': { type: 'boolean' },
|
|
40
|
+
'no-basic-auth': { type: 'boolean' },
|
|
41
|
+
'object-storage': { type: 'boolean' },
|
|
42
|
+
'no-object-storage': { type: 'boolean' },
|
|
43
|
+
'dev-min-scale': { type: 'string' },
|
|
44
|
+
'dev-max-scale': { type: 'string' },
|
|
45
|
+
'staging-min-scale': { type: 'string' },
|
|
46
|
+
'staging-max-scale': { type: 'string' },
|
|
47
|
+
'prod-min-scale': { type: 'string' },
|
|
48
|
+
'prod-max-scale': { type: 'string' },
|
|
49
|
+
config: { type: 'string' },
|
|
50
|
+
advanced: { type: 'boolean' },
|
|
51
|
+
yes: { type: 'boolean' },
|
|
52
|
+
'dry-run': { type: 'boolean' },
|
|
53
|
+
help: { type: 'boolean' },
|
|
54
|
+
version: { type: 'boolean' },
|
|
55
|
+
};
|
|
56
|
+
/** Help text per flag; null hides it (help/version share a closing line). */
|
|
57
|
+
const CLI_HELP = {
|
|
58
|
+
name: { hint: '<name>', text: 'Project name (dns-safe)' },
|
|
59
|
+
dir: { hint: '<path>', text: 'Target directory (default: ./<name>)' },
|
|
60
|
+
region: { hint: '<region>', text: 'fr-par | nl-ams | pl-waw (default: fr-par)' },
|
|
61
|
+
'scw-access-key': { hint: '<key>', text: 'Scaleway access key (env SCW_ACCESS_KEY)' },
|
|
62
|
+
'scw-secret-key': { hint: '<key>', text: 'Scaleway secret key (env SCW_SECRET_KEY)' },
|
|
63
|
+
'scw-project-id': {
|
|
64
|
+
hint: '<id>',
|
|
65
|
+
text: 'Scaleway project ID (env SCW_DEFAULT_PROJECT_ID)',
|
|
66
|
+
},
|
|
67
|
+
'scw-organization-id': {
|
|
68
|
+
hint: '<id>',
|
|
69
|
+
text: 'Scaleway organization ID (env SCW_DEFAULT_ORGANIZATION_ID)',
|
|
70
|
+
},
|
|
71
|
+
'infisical-host': { hint: '<url>', text: 'Infisical host (env INFISICAL_HOST)' },
|
|
72
|
+
'infisical-client-id': {
|
|
73
|
+
hint: '<id>',
|
|
74
|
+
text: 'Machine identity client ID (env INFISICAL_CLIENT_ID)',
|
|
75
|
+
},
|
|
76
|
+
'infisical-client-secret': {
|
|
77
|
+
hint: '<s>',
|
|
78
|
+
text: 'Machine identity secret (env INFISICAL_CLIENT_SECRET)',
|
|
79
|
+
},
|
|
80
|
+
'infisical-project-id': {
|
|
81
|
+
hint: '<id>',
|
|
82
|
+
text: 'Existing Infisical project ID to reuse\n(env INFISICAL_PROJECT_ID; default: create by name)',
|
|
83
|
+
},
|
|
84
|
+
'infisical-project-name': { hint: '<n>', text: 'Infisical project name (default: project name)' },
|
|
85
|
+
'github-token': { hint: '<token>', text: 'GitHub token, repo+workflow (env GITHUB_TOKEN)' },
|
|
86
|
+
'repo-name': { hint: '<name>', text: 'GitHub repository name (default: project name)' },
|
|
87
|
+
private: { text: 'Create the GitHub repository as private' },
|
|
88
|
+
public: { text: 'Create the GitHub repository as public (default)' },
|
|
89
|
+
environments: {
|
|
90
|
+
hint: '<preset>',
|
|
91
|
+
text: 'prod | staging+prod | dev+staging+prod\n(or a list like "dev,staging,prod"; default staging+prod)',
|
|
92
|
+
},
|
|
93
|
+
'basic-auth': { text: 'Enable Basic Auth on non-production environments (default)' },
|
|
94
|
+
'no-basic-auth': { text: 'Disable Basic Auth on non-production environments' },
|
|
95
|
+
'object-storage': { text: 'Provision a per-environment Object Storage bucket' },
|
|
96
|
+
'no-object-storage': { text: 'Do not provision Object Storage (default)' },
|
|
97
|
+
'dev-min-scale': { hint: '<n>', text: 'Dev min instances (default 0)' },
|
|
98
|
+
'dev-max-scale': { hint: '<n>', text: 'Dev max instances (default 1)' },
|
|
99
|
+
'staging-min-scale': { hint: '<n>', text: 'Staging min instances (default 0)' },
|
|
100
|
+
'staging-max-scale': { hint: '<n>', text: 'Staging max instances (default 1)' },
|
|
101
|
+
'prod-min-scale': { hint: '<n>', text: 'Prod min instances (default 0)' },
|
|
102
|
+
'prod-max-scale': { hint: '<n>', text: 'Prod max instances (default 1)' },
|
|
103
|
+
config: { hint: '<file.json>', text: 'Read answers from a JSON config file' },
|
|
104
|
+
advanced: { text: 'Also ask scaling questions interactively' },
|
|
105
|
+
yes: { text: 'Accept defaults, no confirmation prompt' },
|
|
106
|
+
'dry-run': { text: 'Generate files locally, touch no account' },
|
|
107
|
+
help: null,
|
|
108
|
+
version: null,
|
|
109
|
+
};
|
|
110
|
+
const HELP_COLUMN = 33;
|
|
111
|
+
function buildHelp() {
|
|
112
|
+
const lines = [
|
|
113
|
+
'keel: generate and bootstrap serverless infra on Scaleway',
|
|
114
|
+
'',
|
|
115
|
+
'Usage:',
|
|
116
|
+
' npx @gambi97/keel-cli [options]',
|
|
117
|
+
'',
|
|
118
|
+
'Options:',
|
|
119
|
+
];
|
|
120
|
+
for (const [flag, entry] of Object.entries(CLI_HELP)) {
|
|
121
|
+
if (!entry)
|
|
122
|
+
continue;
|
|
123
|
+
const head = ` --${flag}${entry.hint ? ` ${entry.hint}` : ''}`;
|
|
124
|
+
const [first, ...rest] = entry.text.split('\n');
|
|
125
|
+
lines.push(head.padEnd(HELP_COLUMN) + first);
|
|
126
|
+
for (const continuation of rest) {
|
|
127
|
+
lines.push(' '.repeat(HELP_COLUMN) + continuation);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
lines.push(' --help, --version');
|
|
131
|
+
return `${lines.join('\n')}\n`;
|
|
132
|
+
}
|
|
45
133
|
function parseCli(argv) {
|
|
46
|
-
const { values } = parseArgs({
|
|
47
|
-
args: argv,
|
|
48
|
-
options: {
|
|
49
|
-
name: { type: 'string' },
|
|
50
|
-
dir: { type: 'string' },
|
|
51
|
-
region: { type: 'string' },
|
|
52
|
-
'scw-access-key': { type: 'string' },
|
|
53
|
-
'scw-secret-key': { type: 'string' },
|
|
54
|
-
'scw-project-id': { type: 'string' },
|
|
55
|
-
'scw-organization-id': { type: 'string' },
|
|
56
|
-
'infisical-host': { type: 'string' },
|
|
57
|
-
'infisical-client-id': { type: 'string' },
|
|
58
|
-
'infisical-client-secret': { type: 'string' },
|
|
59
|
-
'infisical-project-name': { type: 'string' },
|
|
60
|
-
'github-token': { type: 'string' },
|
|
61
|
-
'repo-name': { type: 'string' },
|
|
62
|
-
private: { type: 'boolean' },
|
|
63
|
-
public: { type: 'boolean' },
|
|
64
|
-
'basic-auth': { type: 'boolean' },
|
|
65
|
-
'no-basic-auth': { type: 'boolean' },
|
|
66
|
-
'staging-min-scale': { type: 'string' },
|
|
67
|
-
'staging-max-scale': { type: 'string' },
|
|
68
|
-
'prod-min-scale': { type: 'string' },
|
|
69
|
-
'prod-max-scale': { type: 'string' },
|
|
70
|
-
config: { type: 'string' },
|
|
71
|
-
advanced: { type: 'boolean' },
|
|
72
|
-
yes: { type: 'boolean' },
|
|
73
|
-
'dry-run': { type: 'boolean' },
|
|
74
|
-
help: { type: 'boolean' },
|
|
75
|
-
version: { type: 'boolean' },
|
|
76
|
-
},
|
|
77
|
-
});
|
|
134
|
+
const { values } = parseArgs({ args: argv, options: CLI_OPTIONS });
|
|
78
135
|
if (values.help) {
|
|
79
|
-
process.stdout.write(
|
|
136
|
+
process.stdout.write(buildHelp());
|
|
80
137
|
process.exit(0);
|
|
81
138
|
}
|
|
82
139
|
if (values.version) {
|
|
@@ -101,6 +158,7 @@ function parseCli(argv) {
|
|
|
101
158
|
host: values['infisical-host'],
|
|
102
159
|
clientId: values['infisical-client-id'],
|
|
103
160
|
clientSecret: values['infisical-client-secret'],
|
|
161
|
+
projectId: values['infisical-project-id'],
|
|
104
162
|
projectName: values['infisical-project-name'],
|
|
105
163
|
},
|
|
106
164
|
github: {
|
|
@@ -108,12 +166,16 @@ function parseCli(argv) {
|
|
|
108
166
|
repoName: values['repo-name'],
|
|
109
167
|
repoPrivate: values.private ? true : values.public ? false : undefined,
|
|
110
168
|
},
|
|
111
|
-
|
|
169
|
+
environments: values.environments ? parseEnvironments(values.environments) : undefined,
|
|
170
|
+
basicAuth: values['no-basic-auth'] ? false : values['basic-auth'],
|
|
171
|
+
objectStorage: values['no-object-storage'] ? false : values['object-storage'],
|
|
112
172
|
scaling: {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
173
|
+
dev: { minScale: num(values['dev-min-scale']), maxScale: num(values['dev-max-scale']) },
|
|
174
|
+
staging: {
|
|
175
|
+
minScale: num(values['staging-min-scale']),
|
|
176
|
+
maxScale: num(values['staging-max-scale']),
|
|
177
|
+
},
|
|
178
|
+
prod: { minScale: num(values['prod-min-scale']), maxScale: num(values['prod-max-scale']) },
|
|
117
179
|
},
|
|
118
180
|
};
|
|
119
181
|
return {
|
|
@@ -128,11 +190,21 @@ function parseCli(argv) {
|
|
|
128
190
|
/** Config files use the same nested shape as PartialAnswers. */
|
|
129
191
|
function normalizeConfigFile(raw) {
|
|
130
192
|
const obj = (raw ?? {});
|
|
193
|
+
let environments;
|
|
194
|
+
if (typeof obj.environments === 'string') {
|
|
195
|
+
environments = parseEnvironments(obj.environments);
|
|
196
|
+
}
|
|
197
|
+
else if (Array.isArray(obj.environments)) {
|
|
198
|
+
environments = obj.environments;
|
|
199
|
+
}
|
|
131
200
|
return mergeAnswers({
|
|
132
201
|
projectName: obj.projectName,
|
|
133
202
|
region: obj.region,
|
|
134
203
|
targetDir: obj.targetDir,
|
|
135
|
-
basicAuthStaging
|
|
204
|
+
// `basicAuthStaging` is accepted as a legacy alias for `basicAuth`.
|
|
205
|
+
basicAuth: (obj.basicAuth ?? obj.basicAuthStaging),
|
|
206
|
+
objectStorage: obj.objectStorage,
|
|
207
|
+
environments,
|
|
136
208
|
scaleway: (obj.scaleway ?? {}),
|
|
137
209
|
infisical: (obj.infisical ?? {}),
|
|
138
210
|
github: (obj.github ?? {}),
|
|
@@ -158,67 +230,106 @@ function checkEnvironment() {
|
|
|
158
230
|
}
|
|
159
231
|
}
|
|
160
232
|
function printDryRunPlan(answers) {
|
|
233
|
+
const envList = answers.environments.map((e) => e.slug).join(', ');
|
|
234
|
+
const ghEnvs = answers.environments.map((e) => e.githubEnvironment).join('/');
|
|
161
235
|
log.info([
|
|
162
236
|
'Dry run: generated the repository locally. A real run would additionally:',
|
|
163
|
-
` - Scaleway: create
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
237
|
+
` - Scaleway: create the Terraform state bucket "${answers.stateBucket}" (${answers.region}),`,
|
|
238
|
+
' restricted by a bucket policy to the identity behind your API key',
|
|
239
|
+
` - Infisical: ${answers.infisical.projectId
|
|
240
|
+
? `reuse project ${answers.infisical.projectId}`
|
|
241
|
+
: `create/reuse project "${answers.infisical.projectName}"`}, environments ${envList},`,
|
|
242
|
+
' seed BASIC_AUTH_USER/BASIC_AUTH_PASSWORD (non-prod) and DATABASE_URL placeholders' +
|
|
243
|
+
(answers.objectStorage ? ' and S3_* placeholders' : ''),
|
|
244
|
+
` - GitHub: create ${answers.github.repoPrivate ? 'private' : 'public'} repo "${answers.github.repoName}", push, set ${CI_SECRET_NAMES.length} encrypted secrets,`,
|
|
245
|
+
` ${CI_VARIABLE_NAMES.length} variables, ${ghEnvs} environments and main branch protection`,
|
|
168
246
|
].join('\n'));
|
|
169
247
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
248
|
+
/**
|
|
249
|
+
* The bootstrap pipeline, in order. Each step is idempotent on the provider
|
|
250
|
+
* side and recorded in the state file, so a re-run after a failure skips what
|
|
251
|
+
* is done and resumes exactly where it stopped.
|
|
252
|
+
*/
|
|
253
|
+
const BOOTSTRAP_STEPS = [
|
|
254
|
+
{
|
|
255
|
+
name: 'scaleway-bucket',
|
|
256
|
+
label: () => 'Creating Terraform state bucket',
|
|
257
|
+
skipMessage: 'Scaleway state bucket: already done, skipping.',
|
|
258
|
+
run: async (answers) => {
|
|
259
|
+
const { policyWarning } = await ensureStateBucket(answers);
|
|
260
|
+
return policyWarning ? { warning: policyWarning } : undefined;
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'infisical',
|
|
265
|
+
label: () => 'Bootstrapping Infisical project and secrets',
|
|
266
|
+
skipMessage: 'Infisical project: already done, skipping.',
|
|
267
|
+
run: async (answers) => {
|
|
268
|
+
const { projectId } = await bootstrapInfisical(answers);
|
|
269
|
+
return { data: { projectId } };
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'github-repo',
|
|
274
|
+
label: (_answers, ctx) => `Creating GitHub repository ${ctx.owner}/${ctx.repo}`,
|
|
275
|
+
skipMessage: 'GitHub repository: already done, skipping.',
|
|
276
|
+
run: async (_answers, ctx) => {
|
|
277
|
+
const { url } = await ensureRepo(ctx);
|
|
278
|
+
return { data: { url } };
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'github-push',
|
|
283
|
+
label: () => 'Pushing generated code to GitHub',
|
|
284
|
+
skipMessage: 'GitHub push: already done, skipping.',
|
|
285
|
+
run: async (answers, ctx) => {
|
|
286
|
+
pushRepo(ctx, answers.github.token, answers.targetDir);
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'github-config',
|
|
291
|
+
label: () => 'Setting GitHub secrets, variables and branch protection',
|
|
292
|
+
skipMessage: 'GitHub configuration: already done, skipping.',
|
|
293
|
+
run: async (answers, ctx, state) => {
|
|
294
|
+
// Recorded by the infisical step (this run or a resumed one).
|
|
295
|
+
await configureRepo(ctx, answers, stepData(state, 'infisical', 'projectId'));
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
async function runBootstrap(answers, state, options) {
|
|
174
300
|
let ctx;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
ctx = await createContext(answers);
|
|
179
|
-
});
|
|
180
|
-
if (!isDone(state, 'scaleway-bucket')) {
|
|
181
|
-
await withSpinner('Creating Terraform state bucket', () => ensureStateBucket(answers));
|
|
182
|
-
markDone(dir, state, 'scaleway-bucket');
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
log.info('Scaleway state bucket: already done, skipping.');
|
|
186
|
-
}
|
|
187
|
-
let infisicalProjectId = stepData(state, 'infisical', 'projectId');
|
|
188
|
-
if (!infisicalProjectId) {
|
|
189
|
-
const result = await withSpinner('Bootstrapping Infisical project and secrets', () => bootstrapInfisical(answers));
|
|
190
|
-
infisicalProjectId = result.projectId;
|
|
191
|
-
markDone(dir, state, 'infisical', { projectId: infisicalProjectId });
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
log.info('Infisical project: already done, skipping.');
|
|
195
|
-
}
|
|
196
|
-
let repoUrl = stepData(state, 'github-repo', 'url');
|
|
197
|
-
if (!repoUrl) {
|
|
198
|
-
const repo = await withSpinner(`Creating GitHub repository ${ctx.owner}/${ctx.repo}`, () => ensureRepo(ctx));
|
|
199
|
-
repoUrl = repo.url;
|
|
200
|
-
markDone(dir, state, 'github-repo', { url: repoUrl });
|
|
301
|
+
if (options.preValidated) {
|
|
302
|
+
// Interactive runs validated each provider inline while prompting; only
|
|
303
|
+
// the GitHub context (octokit + owner) needs to be rebuilt here.
|
|
304
|
+
ctx = await createContext(answers.github);
|
|
201
305
|
}
|
|
202
306
|
else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
307
|
+
// All three credentials are checked before anything is created anywhere,
|
|
308
|
+
// so a bad token cannot leave a half-bootstrapped account behind.
|
|
309
|
+
await withSpinner('Validating Scaleway, Infisical and GitHub credentials', async () => {
|
|
310
|
+
await validateScalewayCredentials(answers.scaleway);
|
|
311
|
+
await validateInfisical(answers.infisical);
|
|
312
|
+
ctx = await createContext(answers.github);
|
|
313
|
+
// A repo with commits would make the push fail after the bucket and the
|
|
314
|
+
// Infisical project were already created: fail here instead. Skipped on
|
|
315
|
+
// resume, where the previous run's push is the reason it is non-empty.
|
|
316
|
+
if (!isDone(state, 'github-push')) {
|
|
317
|
+
const { state: repoState } = await inspectRepo(ctx);
|
|
318
|
+
assertRepoUsable(ctx, repoState);
|
|
319
|
+
}
|
|
208
320
|
});
|
|
209
|
-
markDone(dir, state, 'github-push');
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
log.info('GitHub push: already done, skipping.');
|
|
213
321
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
322
|
+
for (const step of BOOTSTRAP_STEPS) {
|
|
323
|
+
if (isDone(state, step.name)) {
|
|
324
|
+
log.info(step.skipMessage);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const result = await withSpinner(step.label(answers, ctx), () => step.run(answers, ctx, state));
|
|
328
|
+
markDone(answers.targetDir, state, step.name, result?.data);
|
|
329
|
+
if (result?.warning)
|
|
330
|
+
log.warn(result.warning);
|
|
220
331
|
}
|
|
221
|
-
return
|
|
332
|
+
return stepData(state, 'github-repo', 'url');
|
|
222
333
|
}
|
|
223
334
|
async function main() {
|
|
224
335
|
const { partial, flags } = parseCli(process.argv.slice(2));
|
|
@@ -227,7 +338,7 @@ async function main() {
|
|
|
227
338
|
const interactive = process.stdin.isTTY && !flags.yes;
|
|
228
339
|
let collected = partial;
|
|
229
340
|
if (interactive) {
|
|
230
|
-
collected = await fillMissing(partial, { advanced: flags.advanced });
|
|
341
|
+
collected = await fillMissing(partial, { advanced: flags.advanced, dryRun: flags.dryRun });
|
|
231
342
|
}
|
|
232
343
|
else if (!flags.dryRun) {
|
|
233
344
|
const missing = missingRequired(partial);
|
|
@@ -277,8 +388,8 @@ async function main() {
|
|
|
277
388
|
outro('Dry run complete. No account was touched.');
|
|
278
389
|
return;
|
|
279
390
|
}
|
|
280
|
-
const repoUrl = await runBootstrap(answers, state);
|
|
281
|
-
outro(renderNextSteps(answers, repoUrl, `rg.${answers.region}.scw.cloud/${answers.projectName}
|
|
391
|
+
const repoUrl = await runBootstrap(answers, state, { preValidated: interactive });
|
|
392
|
+
outro(renderNextSteps(answers, repoUrl, `rg.${answers.region}.scw.cloud/${answers.projectName}-${answers.environments[0].slug}`));
|
|
282
393
|
}
|
|
283
394
|
main().catch((error) => {
|
|
284
395
|
if (error instanceof ConfigError || error instanceof GenerateError) {
|