@zenifra/cli 0.3.4 → 0.5.0
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 +67 -4
- package/bin/zenifra.mjs +882 -36
- package/examples/http-git-project.json +38 -0
- package/examples/http-github-release-project.json +42 -0
- package/examples/http-github-tag-project.json +42 -0
- package/package.json +1 -1
package/bin/zenifra.mjs
CHANGED
|
@@ -37,6 +37,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
37
37
|
'challengeToken',
|
|
38
38
|
'code',
|
|
39
39
|
'connectionFile',
|
|
40
|
+
'connection',
|
|
40
41
|
'commitSha',
|
|
41
42
|
'config',
|
|
42
43
|
'count',
|
|
@@ -50,6 +51,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
50
51
|
'from',
|
|
51
52
|
'help',
|
|
52
53
|
'image',
|
|
54
|
+
'includePrereleases',
|
|
53
55
|
'idempotencyKey',
|
|
54
56
|
'instance',
|
|
55
57
|
'interval',
|
|
@@ -70,9 +72,11 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
70
72
|
'plan',
|
|
71
73
|
'profile',
|
|
72
74
|
'project',
|
|
75
|
+
'repository',
|
|
73
76
|
'revoke',
|
|
74
77
|
'showValues',
|
|
75
78
|
'status',
|
|
79
|
+
'tagPattern',
|
|
76
80
|
'timeout',
|
|
77
81
|
'to',
|
|
78
82
|
'totp',
|
|
@@ -114,6 +118,30 @@ const ALLOWED_PLAN_VALUES = new Set([
|
|
|
114
118
|
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
|
|
115
119
|
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse']);
|
|
116
120
|
const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
|
|
121
|
+
const ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS = new Set(['tag', 'release']);
|
|
122
|
+
const ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS = new Set(['enabled', 'event', 'tag_pattern', 'include_prereleases']);
|
|
123
|
+
const GITHUB_TAG_PATTERN_BLOCKED_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\[\]{}\\]/u;
|
|
124
|
+
const GITHUB_PUBLIC_CONFIG_FIELDS = [
|
|
125
|
+
'repository_owner',
|
|
126
|
+
'repository_name',
|
|
127
|
+
'branch',
|
|
128
|
+
'runtime',
|
|
129
|
+
'version',
|
|
130
|
+
'auto_deploy',
|
|
131
|
+
'start_command',
|
|
132
|
+
'pre_build_command',
|
|
133
|
+
'build_command',
|
|
134
|
+
];
|
|
135
|
+
const GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS = ['enabled', 'event', 'tag_pattern', 'include_prereleases'];
|
|
136
|
+
const GIT_BUILD_SETTINGS_FIELDS = [
|
|
137
|
+
'runtime',
|
|
138
|
+
'version',
|
|
139
|
+
'start_command',
|
|
140
|
+
'pre_build_command',
|
|
141
|
+
'build_command',
|
|
142
|
+
'dockerfile_path',
|
|
143
|
+
'context_path',
|
|
144
|
+
];
|
|
117
145
|
const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
|
|
118
146
|
const GITHUB_RUNTIME_VERSIONS = {
|
|
119
147
|
nodejs: ['24', '22', '20'],
|
|
@@ -160,9 +188,10 @@ const ANSI = {
|
|
|
160
188
|
};
|
|
161
189
|
|
|
162
190
|
class CliError extends Error {
|
|
163
|
-
constructor(message, exitCode = 1) {
|
|
191
|
+
constructor(message, exitCode = 1, httpStatus) {
|
|
164
192
|
super(message);
|
|
165
193
|
this.exitCode = exitCode;
|
|
194
|
+
if (httpStatus !== undefined) this.httpStatus = httpStatus;
|
|
166
195
|
}
|
|
167
196
|
}
|
|
168
197
|
|
|
@@ -184,6 +213,11 @@ Usage:
|
|
|
184
213
|
zenifra orgs [--json]
|
|
185
214
|
zenifra org set [--org <id>]
|
|
186
215
|
zenifra plans [--type <all|http|database|storage|valkey>] [--json]
|
|
216
|
+
zenifra git providers [--json]
|
|
217
|
+
zenifra git runtimes [--json]
|
|
218
|
+
zenifra git connections [--json]
|
|
219
|
+
zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]
|
|
220
|
+
zenifra git branches --connection <id> --repository <opaque-id> [--json]
|
|
187
221
|
zenifra create project
|
|
188
222
|
zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
|
|
189
223
|
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
|
|
@@ -205,6 +239,11 @@ Usage:
|
|
|
205
239
|
zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--json]
|
|
206
240
|
zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
|
|
207
241
|
zenifra project image set --project <id> --image <image> [--json]
|
|
242
|
+
zenifra project github --project <id> [--json]
|
|
243
|
+
zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
|
|
244
|
+
zenifra project source --project <id> [--json]
|
|
245
|
+
zenifra project source branches --project <id> [--json]
|
|
246
|
+
zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
|
|
208
247
|
zenifra project exposure set --project <id> --exposure <public|private> [--json]
|
|
209
248
|
zenifra project envs --project <id> [--json] [--show-values]
|
|
210
249
|
zenifra project env add --project <id> --name <name> --value <value> [--json]
|
|
@@ -381,6 +420,68 @@ const HELP_SPECS = [
|
|
|
381
420
|
output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
|
|
382
421
|
jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
|
|
383
422
|
},
|
|
423
|
+
{
|
|
424
|
+
command: 'git',
|
|
425
|
+
usage: 'zenifra git\n zenifra git providers [--json]\n zenifra git runtimes [--json]\n zenifra git connections [--json]\n zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]\n zenifra git branches --connection <id> --repository <opaque-id> [--json]',
|
|
426
|
+
description: 'Consulta provedores, runtimes, conexoes e repositorios Git disponiveis na organizacao.',
|
|
427
|
+
examples: ['zenifra git providers', 'zenifra git connections', 'zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao'],
|
|
428
|
+
output: 'Zenifra CLI - git',
|
|
429
|
+
notes: ['A conexao Forgejo e criada no Console por uma pessoa proprietaria da organizacao. A CLI lista e usa conexoes existentes.'],
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
command: 'git providers',
|
|
433
|
+
usage: 'zenifra git providers [--json]',
|
|
434
|
+
description: 'Lista provedores Git e as capacidades disponiveis para a organizacao selecionada.',
|
|
435
|
+
flags: ['--json Imprime a resposta publica da API em JSON.'],
|
|
436
|
+
examples: ['zenifra git providers', 'zenifra git providers --json'],
|
|
437
|
+
output: 'Provedor Disponivel Descoberta Deploy por branch Deploy por versao\nforgejo sim nao sim sim',
|
|
438
|
+
jsonOutput: '{"status":"success","data":{"api_version":1,"providers":[{"id":"forgejo","available":true,"capabilities":{"repositoryDiscovery":false,"pushDeploy":true,"nativePreviews":false,"versionDeploy":true}}]}}',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
command: 'git runtimes',
|
|
442
|
+
usage: 'zenifra git runtimes [--json]',
|
|
443
|
+
description: 'Lista runtimes e versoes disponiveis para builds de repositorios Git.',
|
|
444
|
+
flags: ['--json Imprime a resposta publica da API em JSON.'],
|
|
445
|
+
examples: ['zenifra git runtimes', 'zenifra git runtimes --json'],
|
|
446
|
+
output: 'Runtime Versao padrao Versoes\nnodejs 24 24, 22, 20',
|
|
447
|
+
jsonOutput: '{"status":"success","data":[{"id":"nodejs","label":"Node.js","default_version":"24","default_port":3000,"default_start_command":"npm start","default_build_command":"npm run build","versions":[{"id":"24","label":"24"}]}]}',
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
command: 'git connections',
|
|
451
|
+
usage: 'zenifra git connections [--json]',
|
|
452
|
+
description: 'Lista as conexoes Git disponiveis para a organizacao selecionada.',
|
|
453
|
+
flags: ['--json Imprime as conexoes publicas em JSON.'],
|
|
454
|
+
examples: ['zenifra git connections', 'zenifra git connections --json'],
|
|
455
|
+
output: 'ID Provedor Nome Status\nconnection-1 forgejo Forgejo team active',
|
|
456
|
+
jsonOutput: '{"status":"success","data":[{"id":"connection-1","provider_id":"forgejo","display_name":"Forgejo team","status":"active","capabilities":{"repositoryDiscovery":false,"pushDeploy":true,"nativePreviews":false,"versionDeploy":true}}]}',
|
|
457
|
+
notes: ['A conexao precisa ter sido criada por uma pessoa proprietaria no Console. As credenciais nao aparecem na resposta.'],
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
command: 'git repositories',
|
|
461
|
+
usage: 'zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]',
|
|
462
|
+
description: 'Agrupa comandos para resolver repositorios por caminho explicito.',
|
|
463
|
+
examples: ['zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao'],
|
|
464
|
+
output: 'Zenifra CLI - git repositories',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
command: 'git repositories resolve',
|
|
468
|
+
usage: 'zenifra git repositories resolve --connection <id> --path <owner/repo> [--json]',
|
|
469
|
+
description: 'Valida e resolve o caminho explicito de um repositorio na conexao selecionada.',
|
|
470
|
+
flags: ['--connection <id> ID da conexao Git da organizacao.', '--path <owner/repo> Caminho explicito do repositorio.', '--json Imprime o resultado em JSON.'],
|
|
471
|
+
examples: ['zenifra git repositories resolve --connection conn-1 --path equipe/aplicacao --json'],
|
|
472
|
+
output: 'ID Caminho Branch padrao Privado\nrepo_opaque_1 equipe/aplicacao main sim',
|
|
473
|
+
jsonOutput: '{"status":"success","data":{"id":"repository-id","path":"equipe/aplicacao","default_branch":"main","private":true}}',
|
|
474
|
+
notes: ['Use o ID opaco retornado somente com a mesma conexao.'],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
command: 'git branches',
|
|
478
|
+
usage: 'zenifra git branches --connection <id> --repository <opaque-id> [--json]',
|
|
479
|
+
description: 'Lista branches de um repositorio resolvido pela conexao informada.',
|
|
480
|
+
flags: ['--connection <id> ID da conexao Git da organizacao.', '--repository <id> ID opaco retornado por git repositories resolve.', '--json Imprime a resposta em JSON.'],
|
|
481
|
+
examples: ['zenifra git branches --connection conn-1 --repository repository-id'],
|
|
482
|
+
output: 'Branch Commit\nmain abc123',
|
|
483
|
+
jsonOutput: '{"status":"success","data":[{"name":"main","commit_sha":"<commit-sha>"}]}',
|
|
484
|
+
},
|
|
384
485
|
{
|
|
385
486
|
command: 'valkey',
|
|
386
487
|
usage: 'zenifra valkey\n zenifra valkey status --project <id> [--json]\n zenifra valkey connection --project <id> [--json]\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
@@ -451,11 +552,95 @@ const HELP_SPECS = [
|
|
|
451
552
|
},
|
|
452
553
|
{
|
|
453
554
|
command: 'project',
|
|
454
|
-
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project stop --project <id> [--json]\n zenifra project resume --project <id> [--json]\n zenifra project delete --project <id> --yes [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project metrics capabilities --project <id> [--json]\n zenifra project healthcheck get --project <id> [--json]\n zenifra project healthcheck set --project <id> --path /health [--json]\n zenifra project healthcheck disable --project <id> [--json]\n zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
555
|
+
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project source --project <id> [--json]\n zenifra project source branches --project <id> [--json]\n zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]\n zenifra project stop --project <id> [--json]\n zenifra project resume --project <id> [--json]\n zenifra project delete --project <id> --yes [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project metrics capabilities --project <id> [--json]\n zenifra project healthcheck get --project <id> [--json]\n zenifra project healthcheck set --project <id> --path /health [--json]\n zenifra project healthcheck disable --project <id> [--json]\n zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
455
556
|
description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
|
|
456
|
-
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
557
|
+
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project source --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
457
558
|
output: 'Zenifra CLI - project',
|
|
458
|
-
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, exposure, autoscaling, billing, envs e instances.'],
|
|
559
|
+
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, github, source, exposure, autoscaling, billing, envs e instances.'],
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
command: 'project source',
|
|
563
|
+
usage: 'zenifra project source --project <id> [--json]\n zenifra project source branches --project <id> [--json]\n zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
564
|
+
description: 'Consulta a origem Git generica de um projeto, lista branches ou altera seu modo de deploy.',
|
|
565
|
+
examples: ['zenifra project source --project proj_1', 'zenifra project source branches --project proj_1'],
|
|
566
|
+
output: 'Zenifra CLI - project source',
|
|
567
|
+
notes: ['Use project github para ler ou alterar configuracoes GitHub legadas.'],
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
command: 'project source branches',
|
|
571
|
+
usage: 'zenifra project source branches --project <id> [--json]',
|
|
572
|
+
description: 'Lista branches disponiveis para a origem Git generica atual do projeto.',
|
|
573
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
574
|
+
examples: ['zenifra project source branches --project proj_1'],
|
|
575
|
+
output: 'Branch Commit\nmain abc123',
|
|
576
|
+
jsonOutput: '{"status":"success","data":[{"name":"main","commit_sha":"<commit-sha>"}]}',
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
command: 'project source deploy-settings',
|
|
580
|
+
usage: 'zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
581
|
+
description: 'Agrupa os modos de deploy da origem Git generica.',
|
|
582
|
+
examples: ['zenifra project source deploy-settings set --project proj_1 --mode branch'],
|
|
583
|
+
output: 'Zenifra CLI - project source deploy-settings',
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
command: 'project source deploy-settings set',
|
|
587
|
+
usage: 'zenifra project source deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
588
|
+
description: 'Define e confirma o modo de deploy da origem Git Forgejo do projeto.',
|
|
589
|
+
flags: [
|
|
590
|
+
'--project <id> ID do projeto.',
|
|
591
|
+
'--mode <manual|branch|tag|release> Modo de deploy exclusivo.',
|
|
592
|
+
'--tag-pattern <pattern> Obrigatorio para tag e release; aceita * e ?.',
|
|
593
|
+
'--include-prereleases <true|false> Inclui prereleases somente no modo release. Padrao: false.',
|
|
594
|
+
'--json Imprime a configuracao confirmada em JSON.',
|
|
595
|
+
],
|
|
596
|
+
examples: [
|
|
597
|
+
'zenifra project source deploy-settings set --project proj_1 --mode manual',
|
|
598
|
+
'zenifra project source deploy-settings set --project proj_1 --mode branch',
|
|
599
|
+
'zenifra project source deploy-settings set --project proj_1 --mode tag --tag-pattern "v*"',
|
|
600
|
+
'zenifra project source deploy-settings set --project proj_1 --mode release --tag-pattern "v*" --include-prereleases true',
|
|
601
|
+
],
|
|
602
|
+
output: 'Configuracao de deploy da origem Git confirmada: release (v*).',
|
|
603
|
+
jsonOutput: '{"status":"success","data":{"source":{"connection_id":"connection-id","repository_id":"repository-id","branch":"main","auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":true},"provider_id":"forgejo","repository_path":"equipe/aplicacao"},"build":{"runtime":"nodejs","version":"24","start_command":"npm start"},"source_revision":2}}',
|
|
604
|
+
notes: [
|
|
605
|
+
'Tag e release exigem --tag-pattern; prereleases so podem ser habilitadas para release.',
|
|
606
|
+
'O comando preserva identidade, branch e configuracao de build da origem e confirma o resultado com uma nova leitura.',
|
|
607
|
+
'A API substitui source e build juntos e nao oferece precondicao de revisao. Evite alterar a origem ao mesmo tempo por outro cliente.',
|
|
608
|
+
'Para projetos com origem GitHub legada, use "zenifra project github deploy-settings set".',
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
command: 'project github',
|
|
613
|
+
usage: 'zenifra project github --project <id> [--json]',
|
|
614
|
+
description: 'Mostra a configuracao GitHub publica e o modo atual de deploy do projeto.',
|
|
615
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a configuracao publica em JSON.'],
|
|
616
|
+
examples: ['zenifra project github --project proj_1', 'zenifra project github --project proj_1 --json'],
|
|
617
|
+
output: 'Repositorio: example-team/sample-app\nBranch: main\nRuntime: nodejs@24\nModo de deploy: release\nPadrao de tags: v*\nIncluir prereleases: nao',
|
|
618
|
+
jsonOutput: '{"repository_owner":"example-team","repository_name":"sample-app","branch":"main","runtime":"nodejs","version":"24","auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":false}}',
|
|
619
|
+
notes: ['A resposta mostra apenas campos publicos da configuracao do projeto.'],
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
command: 'project github deploy-settings set',
|
|
623
|
+
usage: 'zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
|
|
624
|
+
description: 'Define um unico modo de deploy para o projeto com origem GitHub e confirma a configuracao salva.',
|
|
625
|
+
flags: [
|
|
626
|
+
'--project <id> ID do projeto.',
|
|
627
|
+
'--mode <manual|branch|tag|release> Modo de deploy exclusivo.',
|
|
628
|
+
'--tag-pattern <pattern> Padrao obrigatorio para tag e release. Aceita * e ?.',
|
|
629
|
+
'--include-prereleases <true|false> Inclui prereleases somente no modo release. Padrao: false.',
|
|
630
|
+
'--json Imprime a configuracao confirmada em JSON.',
|
|
631
|
+
],
|
|
632
|
+
examples: [
|
|
633
|
+
'zenifra project github deploy-settings set --project proj_1 --mode manual',
|
|
634
|
+
'zenifra project github deploy-settings set --project proj_1 --mode branch',
|
|
635
|
+
'zenifra project github deploy-settings set --project proj_1 --mode tag --tag-pattern "v*"',
|
|
636
|
+
'zenifra project github deploy-settings set --project proj_1 --mode release --tag-pattern "v*" --include-prereleases true',
|
|
637
|
+
],
|
|
638
|
+
output: 'Configuracao de deploy GitHub confirmada: release (v*).',
|
|
639
|
+
jsonOutput: '{"repository_owner":"example-team","repository_name":"sample-app","branch":"main","runtime":"nodejs","version":"24","auto_deploy":false,"version_deploy":{"enabled":true,"event":"release","tag_pattern":"v*","include_prereleases":true}}',
|
|
640
|
+
notes: [
|
|
641
|
+
'Tag e release exigem --tag-pattern explicito; prereleases so podem ser habilitadas no modo release.',
|
|
642
|
+
'A configuracao salva e lida novamente antes de a CLI informar sucesso. O comando deploy manual continua disponivel.',
|
|
643
|
+
],
|
|
459
644
|
},
|
|
460
645
|
{
|
|
461
646
|
command: 'project info',
|
|
@@ -717,7 +902,7 @@ const HELP_SPECS = [
|
|
|
717
902
|
{
|
|
718
903
|
command: 'builds',
|
|
719
904
|
usage: 'zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
720
|
-
description: 'Lista builds
|
|
905
|
+
description: 'Lista builds de um projeto com origem Git.',
|
|
721
906
|
flags: ['--project <id> ID do projeto.', '--page <n> Pagina.', '--limit <n> Itens por pagina.', '--branch <name> Filtra branch.', '--status <status> Filtra status.', '--json Imprime a resposta em JSON.'],
|
|
722
907
|
examples: ['zenifra builds --project 507f1f77bcf86cd799439012 --status success'],
|
|
723
908
|
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
@@ -726,7 +911,7 @@ const HELP_SPECS = [
|
|
|
726
911
|
{
|
|
727
912
|
command: 'builds logs',
|
|
728
913
|
usage: 'zenifra builds logs --project <id> --build <id> [--cursor <n>] [--limit <n>] [--follow] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
729
|
-
description: '
|
|
914
|
+
description: 'Le os logs de um build Git existente e opcionalmente acompanha novos eventos em tempo real.',
|
|
730
915
|
flags: [
|
|
731
916
|
'--project <id> ID do projeto.',
|
|
732
917
|
'--build <id> ID do build.',
|
|
@@ -744,7 +929,7 @@ const HELP_SPECS = [
|
|
|
744
929
|
{
|
|
745
930
|
command: 'deployments',
|
|
746
931
|
usage: 'zenifra deployments --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]',
|
|
747
|
-
description: 'Alias para listar builds
|
|
932
|
+
description: 'Alias para listar builds e deployments Git de um projeto.',
|
|
748
933
|
flags: ['--project <id> ID do projeto.', '--page <n> Pagina.', '--limit <n> Itens por pagina.', '--branch <name> Filtra branch.', '--status <status> Filtra status.', '--json Imprime a resposta em JSON.'],
|
|
749
934
|
examples: ['zenifra deployments --project 507f1f77bcf86cd799439012'],
|
|
750
935
|
output: 'Build Status Branch Commit\nbuild_123 success main abc123',
|
|
@@ -753,7 +938,7 @@ const HELP_SPECS = [
|
|
|
753
938
|
{
|
|
754
939
|
command: 'deploy',
|
|
755
940
|
usage: 'zenifra deploy --project <id> [--branch <name>] [--commit-sha <sha>] [--json]',
|
|
756
|
-
description: 'Dispara um build/deploy
|
|
941
|
+
description: 'Dispara um build/deploy Git para o projeto e retorna o build_id para acompanhamento posterior.',
|
|
757
942
|
flags: ['--project <id> ID do projeto.', '--branch <name> Branch a publicar.', '--commit-sha <sha> Commit especifico.', '--json Imprime a resposta em JSON.'],
|
|
758
943
|
examples: [
|
|
759
944
|
'zenifra deploy --project 507f1f77bcf86cd799439012 --branch main',
|
|
@@ -847,7 +1032,7 @@ function removedProjectsCreateMessage() {
|
|
|
847
1032
|
}
|
|
848
1033
|
|
|
849
1034
|
function isNamespaceCommand(command) {
|
|
850
|
-
return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
|
|
1035
|
+
return ['auth', 'profile', 'project', 'org', 'valkey', 'git'].includes(command);
|
|
851
1036
|
}
|
|
852
1037
|
|
|
853
1038
|
function parseArgs(argv) {
|
|
@@ -861,7 +1046,9 @@ function parseArgs(argv) {
|
|
|
861
1046
|
continue;
|
|
862
1047
|
}
|
|
863
1048
|
|
|
864
|
-
const
|
|
1049
|
+
const argumentValueSeparator = arg.indexOf('=');
|
|
1050
|
+
const rawKey = argumentValueSeparator === -1 ? arg.slice(2) : arg.slice(2, argumentValueSeparator);
|
|
1051
|
+
const inlineValue = argumentValueSeparator === -1 ? undefined : arg.slice(argumentValueSeparator + 1);
|
|
865
1052
|
const key = rawKey.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
866
1053
|
|
|
867
1054
|
if (inlineValue !== undefined) {
|
|
@@ -1218,7 +1405,9 @@ function estimateWizardTotal(state = {}) {
|
|
|
1218
1405
|
total += 1;
|
|
1219
1406
|
|
|
1220
1407
|
if (state.httpSource === 'github') {
|
|
1221
|
-
total +=
|
|
1408
|
+
total += state.httpGithubDeployMode === 'release'
|
|
1409
|
+
? 11
|
|
1410
|
+
: state.httpGithubDeployMode === 'tag' ? 10 : 9;
|
|
1222
1411
|
} else if (state.httpSource === 'oci') {
|
|
1223
1412
|
total += 2;
|
|
1224
1413
|
if (state.httpImagePublic === false) {
|
|
@@ -1635,30 +1824,393 @@ async function request(session, flags, method, path, {
|
|
|
1635
1824
|
|
|
1636
1825
|
if (!response.ok) {
|
|
1637
1826
|
const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
|
|
1827
|
+
const httpError = (errorMessage) => new CliError(errorMessage, 1, response.status);
|
|
1638
1828
|
if (response.status === 401) {
|
|
1639
1829
|
if (credential?.type === 'api_key') {
|
|
1640
|
-
throw
|
|
1830
|
+
throw httpError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
|
|
1641
1831
|
}
|
|
1642
|
-
throw
|
|
1832
|
+
throw httpError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
|
|
1643
1833
|
}
|
|
1644
1834
|
if (response.status === 403) {
|
|
1645
|
-
throw
|
|
1835
|
+
throw httpError(message);
|
|
1646
1836
|
}
|
|
1647
1837
|
if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
|
|
1648
|
-
throw
|
|
1838
|
+
throw httpError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
|
|
1649
1839
|
}
|
|
1650
1840
|
if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
|
|
1651
|
-
throw
|
|
1841
|
+
throw httpError('Este projeto nao possui acesso a metricas.');
|
|
1652
1842
|
}
|
|
1653
1843
|
if (String(message).includes('missing x-organization-id')) {
|
|
1654
|
-
throw
|
|
1844
|
+
throw httpError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
1655
1845
|
}
|
|
1656
|
-
throw
|
|
1846
|
+
throw httpError(message);
|
|
1657
1847
|
}
|
|
1658
1848
|
|
|
1659
1849
|
return payload;
|
|
1660
1850
|
}
|
|
1661
1851
|
|
|
1852
|
+
function hasValidGitProvidersApiV1(payload) {
|
|
1853
|
+
if (payload?.status !== 'success' || !isRecord(payload.data) || payload.data.api_version !== 1 || !Array.isArray(payload.data.providers)) {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
return payload.data.providers.every((provider) => (
|
|
1858
|
+
isRecord(provider)
|
|
1859
|
+
&& typeof provider.id === 'string'
|
|
1860
|
+
&& /^[a-z0-9][a-z0-9_-]{0,63}$/.test(provider.id)
|
|
1861
|
+
&& typeof provider.available === 'boolean'
|
|
1862
|
+
&& isRecord(provider.capabilities)
|
|
1863
|
+
&& typeof provider.capabilities.repositoryDiscovery === 'boolean'
|
|
1864
|
+
&& typeof provider.capabilities.pushDeploy === 'boolean'
|
|
1865
|
+
&& typeof provider.capabilities.nativePreviews === 'boolean'
|
|
1866
|
+
));
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
async function supportsNeutralGitRoutes(session, flags, orgId) {
|
|
1870
|
+
let payload;
|
|
1871
|
+
try {
|
|
1872
|
+
payload = await request(session, flags, 'GET', '/git/providers', { orgId });
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if ([404, 405, 501].includes(error?.httpStatus)) return false;
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (!hasValidGitProvidersApiV1(payload)) {
|
|
1879
|
+
throw new CliError('A API retornou uma resposta invalida para as capacidades de origem Git.');
|
|
1880
|
+
}
|
|
1881
|
+
return true;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function hasLegacyGithubSource(project) {
|
|
1885
|
+
if (!isRecord(project)) return false;
|
|
1886
|
+
// Public Git source projections must never be rewritten as legacy GitHub input.
|
|
1887
|
+
// preview.source is separate preview metadata and does not identify the primary project source.
|
|
1888
|
+
const genericSourceFields = ['source', 'build', 'git_source', 'git_build'];
|
|
1889
|
+
if (genericSourceFields.some((field) => project[field] !== undefined || project.config?.[field] !== undefined)) {
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const github = project.github || project.config?.github;
|
|
1894
|
+
return isRecord(github)
|
|
1895
|
+
&& typeof github.repository_owner === 'string'
|
|
1896
|
+
&& github.repository_owner.trim().length > 0
|
|
1897
|
+
&& typeof github.repository_name === 'string'
|
|
1898
|
+
&& github.repository_name.trim().length > 0;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
async function resolveGitProjectApiPath(session, flags, projectId, orgId) {
|
|
1902
|
+
if (await supportsNeutralGitRoutes(session, flags, orgId)) {
|
|
1903
|
+
return `/project/${projectId}`;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
1907
|
+
if (!hasLegacyGithubSource(project)) {
|
|
1908
|
+
throw new CliError('Esta API antiga so permite builds e deploys de projetos com uma origem GitHub legada compativel.');
|
|
1909
|
+
}
|
|
1910
|
+
return `/project/${projectId}/github`;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function requiredGitOpaqueId(flags, name, commandKey) {
|
|
1914
|
+
const value = flags[name];
|
|
1915
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value)) {
|
|
1916
|
+
printCommandHelpAndFail(commandKey);
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
return value;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function requiredGitRepositoryPath(flags, commandKey) {
|
|
1923
|
+
const value = flags.path;
|
|
1924
|
+
const segments = typeof value === 'string' ? value.split('/') : [];
|
|
1925
|
+
if (typeof value !== 'string'
|
|
1926
|
+
|| value.length < 3
|
|
1927
|
+
|| value.length > 1024
|
|
1928
|
+
|| segments.length < 2
|
|
1929
|
+
|| segments.some((segment) => segment.length === 0)
|
|
1930
|
+
|| /[\u0000-\u001f\u007f-\u009f\\?#%]/u.test(value)) {
|
|
1931
|
+
printCommandHelpAndFail(commandKey);
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
return value;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function yesNo(value) {
|
|
1938
|
+
if (value === true) return 'sim';
|
|
1939
|
+
if (value === false) return 'nao';
|
|
1940
|
+
return 'indisponivel';
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
async function handleGitProviders(session, flags) {
|
|
1944
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1945
|
+
const payload = await request(session, flags, 'GET', '/git/providers', { orgId });
|
|
1946
|
+
if (!hasValidGitProvidersApiV1(payload)) {
|
|
1947
|
+
throw new CliError('A API retornou uma resposta invalida para o catalogo de provedores Git.');
|
|
1948
|
+
}
|
|
1949
|
+
if (flags.json) return printJson(payload);
|
|
1950
|
+
|
|
1951
|
+
printTable(payload.data.providers, [
|
|
1952
|
+
{ label: 'Provedor', value: (provider) => provider.id },
|
|
1953
|
+
{ label: 'Disponivel', value: (provider) => yesNo(provider.available) },
|
|
1954
|
+
{ label: 'Descoberta', value: (provider) => yesNo(provider.capabilities.repositoryDiscovery) },
|
|
1955
|
+
{ label: 'Deploy por branch', value: (provider) => yesNo(provider.capabilities.pushDeploy) },
|
|
1956
|
+
{ label: 'Deploy por versao', value: (provider) => yesNo(provider.capabilities.versionDeploy) },
|
|
1957
|
+
]);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async function handleGitRuntimes(session, flags) {
|
|
1961
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1962
|
+
const payload = await request(session, flags, 'GET', '/git/runtime-catalog', { orgId });
|
|
1963
|
+
const runtimes = unwrapData(payload);
|
|
1964
|
+
if (!Array.isArray(runtimes)) {
|
|
1965
|
+
throw new CliError('A API retornou uma resposta invalida para o catalogo de runtimes Git.');
|
|
1966
|
+
}
|
|
1967
|
+
if (flags.json) return printJson(payload);
|
|
1968
|
+
|
|
1969
|
+
printTable(runtimes, [
|
|
1970
|
+
{ label: 'Runtime', value: (runtime) => runtime.label || runtime.id || '-' },
|
|
1971
|
+
{ label: 'Versao padrao', value: (runtime) => runtime.default_version || '-' },
|
|
1972
|
+
{ label: 'Versoes', value: (runtime) => Array.isArray(runtime.versions) ? runtime.versions.map((version) => version.id).join(', ') : '-' },
|
|
1973
|
+
]);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
async function handleGitConnections(session, flags) {
|
|
1977
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1978
|
+
const payload = await request(session, flags, 'GET', '/git/connections', { orgId });
|
|
1979
|
+
const connections = unwrapData(payload);
|
|
1980
|
+
if (!Array.isArray(connections)) {
|
|
1981
|
+
throw new CliError('A API retornou uma resposta invalida para as conexoes Git.');
|
|
1982
|
+
}
|
|
1983
|
+
if (flags.json) return printJson(payload);
|
|
1984
|
+
|
|
1985
|
+
printTable(connections, [
|
|
1986
|
+
{ label: 'ID', value: (connection) => connection.id || '-' },
|
|
1987
|
+
{ label: 'Provedor', value: (connection) => connection.provider_id || '-' },
|
|
1988
|
+
{ label: 'Nome', value: (connection) => connection.display_name || '-' },
|
|
1989
|
+
{ label: 'Status', value: (connection) => connection.status || '-' },
|
|
1990
|
+
]);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
async function handleGitRepositoryResolve(session, flags) {
|
|
1994
|
+
const connectionId = requiredGitOpaqueId(flags, 'connection', 'git repositories resolve');
|
|
1995
|
+
const repositoryPath = requiredGitRepositoryPath(flags, 'git repositories resolve');
|
|
1996
|
+
if (!connectionId || !repositoryPath) return;
|
|
1997
|
+
|
|
1998
|
+
const orgId = await resolveOrgId(session, flags);
|
|
1999
|
+
const endpoint = '/git/connections/' + encodeURIComponent(connectionId) + '/repositories/resolve';
|
|
2000
|
+
const payload = await request(session, flags, 'POST', endpoint, {
|
|
2001
|
+
orgId,
|
|
2002
|
+
body: { path: repositoryPath },
|
|
2003
|
+
});
|
|
2004
|
+
if (flags.json) return printJson(payload);
|
|
2005
|
+
|
|
2006
|
+
const repository = unwrapData(payload);
|
|
2007
|
+
printTable([repository], [
|
|
2008
|
+
{ label: 'ID', value: (entry) => entry.id || '-' },
|
|
2009
|
+
{ label: 'Caminho', value: (entry) => entry.path || '-' },
|
|
2010
|
+
{ label: 'Branch padrao', value: (entry) => entry.default_branch || '-' },
|
|
2011
|
+
{ label: 'Privado', value: (entry) => yesNo(entry.private) },
|
|
2012
|
+
]);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
async function handleGitBranches(session, flags) {
|
|
2016
|
+
const connectionId = requiredGitOpaqueId(flags, 'connection', 'git branches');
|
|
2017
|
+
const repositoryId = requiredGitOpaqueId(flags, 'repository', 'git branches');
|
|
2018
|
+
if (!connectionId || !repositoryId) return;
|
|
2019
|
+
|
|
2020
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2021
|
+
const endpoint = '/git/connections/' + encodeURIComponent(connectionId)
|
|
2022
|
+
+ '/repositories/' + encodeURIComponent(repositoryId) + '/branches';
|
|
2023
|
+
const payload = await request(session, flags, 'GET', endpoint, { orgId });
|
|
2024
|
+
const branches = unwrapData(payload);
|
|
2025
|
+
if (!Array.isArray(branches)) {
|
|
2026
|
+
throw new CliError('A API retornou uma resposta invalida para as branches Git.');
|
|
2027
|
+
}
|
|
2028
|
+
if (flags.json) return printJson(payload);
|
|
2029
|
+
|
|
2030
|
+
printTable(branches, [
|
|
2031
|
+
{ label: 'Branch', value: (branch) => branch.name || '-' },
|
|
2032
|
+
{ label: 'Commit', value: (branch) => branch.commit_sha || '-' },
|
|
2033
|
+
]);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function printGitProjectSource(data) {
|
|
2037
|
+
if (!isRecord(data?.source)) {
|
|
2038
|
+
process.stdout.write('Este projeto nao possui uma origem Git generica configurada.\n');
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const source = data.source;
|
|
2043
|
+
const build = isRecord(data.build) ? data.build : {};
|
|
2044
|
+
const mode = source.provider_id === 'github' ? null : gitSourceDeploymentMode(source);
|
|
2045
|
+
const rows = [
|
|
2046
|
+
{ field: 'Provedor', value: source.provider_id || '-' },
|
|
2047
|
+
{ field: 'Repositorio', value: source.repository_path || source.repository_id || '-' },
|
|
2048
|
+
{ field: 'Branch', value: source.branch || '-' },
|
|
2049
|
+
{ field: 'Runtime', value: build.runtime ? build.runtime + '@' + (build.version || '-') : '-' },
|
|
2050
|
+
{ field: 'Modo de deploy', value: mode || (source.provider_id === 'github' ? 'consulte project github' : 'indisponivel') },
|
|
2051
|
+
];
|
|
2052
|
+
if (mode === 'tag' || mode === 'release') {
|
|
2053
|
+
rows.push(
|
|
2054
|
+
{ field: 'Padrao de tags', value: source.version_deploy?.tag_pattern || '-' },
|
|
2055
|
+
{ field: 'Incluir prereleases', value: source.version_deploy?.include_prereleases === true ? 'sim' : 'nao' },
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
printTable(rows, [
|
|
2059
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
2060
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
2061
|
+
]);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async function handleProjectSource(session, flags) {
|
|
2065
|
+
const projectId = requireProjectId(flags, 'project source');
|
|
2066
|
+
if (!projectId) return;
|
|
2067
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2068
|
+
const payload = await request(session, flags, 'GET', '/project/' + encodeURIComponent(projectId) + '/source', { orgId });
|
|
2069
|
+
const source = unwrapData(payload);
|
|
2070
|
+
if (flags.json) return printJson(payload);
|
|
2071
|
+
printGitProjectSource(source);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
async function handleProjectSourceBranches(session, flags) {
|
|
2075
|
+
const projectId = requireProjectId(flags, 'project source branches');
|
|
2076
|
+
if (!projectId) return;
|
|
2077
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2078
|
+
const endpoint = '/project/' + encodeURIComponent(projectId) + '/source/branches';
|
|
2079
|
+
const payload = await request(session, flags, 'GET', endpoint, { orgId });
|
|
2080
|
+
const branches = unwrapData(payload);
|
|
2081
|
+
if (!Array.isArray(branches)) {
|
|
2082
|
+
throw new CliError('A API retornou uma resposta invalida para as branches da origem Git.');
|
|
2083
|
+
}
|
|
2084
|
+
if (flags.json) return printJson(payload);
|
|
2085
|
+
printTable(branches, [
|
|
2086
|
+
{ label: 'Branch', value: (branch) => branch.name || '-' },
|
|
2087
|
+
{ label: 'Commit', value: (branch) => branch.commit_sha || '-' },
|
|
2088
|
+
]);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function projectGitSourceInput(source) {
|
|
2092
|
+
if (!isRecord(source)
|
|
2093
|
+
|| typeof source.connection_id !== 'string'
|
|
2094
|
+
|| typeof source.repository_id !== 'string'
|
|
2095
|
+
|| typeof source.branch !== 'string'
|
|
2096
|
+
|| typeof source.auto_deploy !== 'boolean') {
|
|
2097
|
+
throw new CliError('A API retornou uma origem Git incompleta; nenhuma alteracao foi enviada.');
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
const input = {
|
|
2101
|
+
connection_id: source.connection_id,
|
|
2102
|
+
repository_id: source.repository_id,
|
|
2103
|
+
branch: source.branch,
|
|
2104
|
+
auto_deploy: source.auto_deploy,
|
|
2105
|
+
};
|
|
2106
|
+
if (source.version_deploy === undefined || source.version_deploy === null) return input;
|
|
2107
|
+
if (!isRecord(source.version_deploy)) {
|
|
2108
|
+
throw new CliError('A API retornou configuracoes de deploy Git invalidas; nenhuma alteracao foi enviada.');
|
|
2109
|
+
}
|
|
2110
|
+
const versionDeploy = {};
|
|
2111
|
+
for (const field of GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS) {
|
|
2112
|
+
if (Object.hasOwn(source.version_deploy, field)) versionDeploy[field] = source.version_deploy[field];
|
|
2113
|
+
}
|
|
2114
|
+
input.version_deploy = versionDeploy;
|
|
2115
|
+
return input;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function projectGitBuildSettingsInput(build) {
|
|
2119
|
+
if (!isRecord(build)) {
|
|
2120
|
+
throw new CliError('A API retornou configuracoes de build incompletas; nenhuma alteracao foi enviada.');
|
|
2121
|
+
}
|
|
2122
|
+
const input = {};
|
|
2123
|
+
for (const field of GIT_BUILD_SETTINGS_FIELDS) {
|
|
2124
|
+
if (Object.hasOwn(build, field)) input[field] = build[field];
|
|
2125
|
+
}
|
|
2126
|
+
return input;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function gitSourceDeploymentMode(source) {
|
|
2130
|
+
const versionDeployEnabled = source?.version_deploy?.enabled === true;
|
|
2131
|
+
if (source?.auto_deploy === true && versionDeployEnabled) return null;
|
|
2132
|
+
if (source?.auto_deploy === true) return 'branch';
|
|
2133
|
+
if (!versionDeployEnabled) return 'manual';
|
|
2134
|
+
if (!ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(source.version_deploy.event)) return null;
|
|
2135
|
+
return source.version_deploy.event;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function projectGitSourceMatches(data, expectedSource, expectedBuild, desired) {
|
|
2139
|
+
if (!isRecord(data) || !isRecord(data.source) || !isRecord(data.build)) return false;
|
|
2140
|
+
const source = data.source;
|
|
2141
|
+
if (source.provider_id !== 'forgejo'
|
|
2142
|
+
|| source.connection_id !== expectedSource.connection_id
|
|
2143
|
+
|| source.repository_id !== expectedSource.repository_id
|
|
2144
|
+
|| source.branch !== expectedSource.branch
|
|
2145
|
+
|| gitSourceDeploymentMode(source) !== desired.mode) return false;
|
|
2146
|
+
|
|
2147
|
+
if (desired.mode === 'tag' || desired.mode === 'release') {
|
|
2148
|
+
if (source.version_deploy?.tag_pattern !== desired.body.version_deploy.tag_pattern
|
|
2149
|
+
|| (source.version_deploy?.include_prereleases === true) !== desired.includePrereleases) return false;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
return Object.entries(expectedBuild).every(([field, value]) => data.build[field] === value);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
async function handleProjectSourceDeploySettingsSet(session, flags) {
|
|
2156
|
+
const desired = parseGithubDeploySettings(flags);
|
|
2157
|
+
const projectId = requireProjectId(flags, 'project source deploy-settings set');
|
|
2158
|
+
if (!projectId) return;
|
|
2159
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2160
|
+
const endpoint = '/project/' + encodeURIComponent(projectId) + '/source';
|
|
2161
|
+
let current;
|
|
2162
|
+
try {
|
|
2163
|
+
current = unwrapData(await request(session, flags, 'GET', endpoint, { orgId }));
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
if (error?.httpStatus === 404) {
|
|
2166
|
+
throw new CliError('A origem Git generica nao esta disponivel para este projeto ou API. Para projetos GitHub legados, use "zenifra project github deploy-settings set".', 1, 404);
|
|
2167
|
+
}
|
|
2168
|
+
throw error;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
if (!isRecord(current?.source) || !isRecord(current?.build)) {
|
|
2172
|
+
throw new CliError('Este projeto nao tem uma origem Git generica configurada. Configure-a antes de alterar o modo de deploy.');
|
|
2173
|
+
}
|
|
2174
|
+
if (current.source.provider_id === 'github') {
|
|
2175
|
+
throw new CliError('Esta origem GitHub nao pode ser alterada por este comando. Consulte e altere com "zenifra project github deploy-settings set".');
|
|
2176
|
+
}
|
|
2177
|
+
if (current.source.provider_id !== 'forgejo') {
|
|
2178
|
+
throw new CliError('O provedor da origem nao e suportado por este comando; nenhuma alteracao foi enviada.');
|
|
2179
|
+
}
|
|
2180
|
+
if (desired.mode === 'branch' && current.capabilities?.pushDeploy !== true) {
|
|
2181
|
+
throw new CliError('A origem Git nao oferece deploy automatico por branch.');
|
|
2182
|
+
}
|
|
2183
|
+
if ((desired.mode === 'tag' || desired.mode === 'release') && current.capabilities?.versionDeploy !== true) {
|
|
2184
|
+
throw new CliError('A origem Git nao oferece deploy automatico por tag ou release.');
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const source = projectGitSourceInput(current.source);
|
|
2188
|
+
const build = projectGitBuildSettingsInput(current.build);
|
|
2189
|
+
source.auto_deploy = desired.body.auto_deploy;
|
|
2190
|
+
delete source.version_deploy;
|
|
2191
|
+
if (desired.mode === 'tag' || desired.mode === 'release') {
|
|
2192
|
+
source.version_deploy = desired.body.version_deploy;
|
|
2193
|
+
}
|
|
2194
|
+
const expectedSource = {
|
|
2195
|
+
connection_id: source.connection_id,
|
|
2196
|
+
repository_id: source.repository_id,
|
|
2197
|
+
branch: source.branch,
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
await request(session, flags, 'PUT', endpoint, {
|
|
2201
|
+
orgId,
|
|
2202
|
+
body: { source, build },
|
|
2203
|
+
});
|
|
2204
|
+
const confirmed = unwrapData(await request(session, flags, 'GET', endpoint, { orgId }));
|
|
2205
|
+
if (!projectGitSourceMatches(confirmed, expectedSource, build, desired)) {
|
|
2206
|
+
throw new CliError('A configuracao solicitada nao foi confirmada pela API. Consulte "zenifra project source".');
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
if (flags.json) return printJson({ status: 'success', data: confirmed });
|
|
2210
|
+
process.stdout.write('Configuracao de deploy da origem Git confirmada: ' + desired.mode
|
|
2211
|
+
+ ((desired.mode === 'tag' || desired.mode === 'release') ? ' (' + desired.body.version_deploy.tag_pattern + ')' : '') + '.\n');
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1662
2214
|
function unwrapData(payload) {
|
|
1663
2215
|
return payload?.data ?? payload;
|
|
1664
2216
|
}
|
|
@@ -2943,6 +3495,57 @@ function validateValkeyConfig(config, plan, catalog) {
|
|
|
2943
3495
|
};
|
|
2944
3496
|
}
|
|
2945
3497
|
|
|
3498
|
+
function isValidGithubTagPattern(value) {
|
|
3499
|
+
if (typeof value !== 'string') return false;
|
|
3500
|
+
const length = Array.from(value).length;
|
|
3501
|
+
return length >= 1 && length <= 255 && value.trim().length > 0
|
|
3502
|
+
&& !GITHUB_TAG_PATTERN_BLOCKED_CHARACTERS.test(value);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
function validateGithubVersionDeployConfig(github) {
|
|
3506
|
+
if (!isRecord(github)) {
|
|
3507
|
+
throw new CliError('config.github deve ser um objeto.');
|
|
3508
|
+
}
|
|
3509
|
+
if (github.auto_deploy !== undefined && typeof github.auto_deploy !== 'boolean') {
|
|
3510
|
+
throw new CliError('config.github.auto_deploy deve ser true ou false.');
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
const versionDeploy = github.version_deploy;
|
|
3514
|
+
if (versionDeploy === undefined) return;
|
|
3515
|
+
if (!isRecord(versionDeploy)) {
|
|
3516
|
+
throw new CliError('config.github.version_deploy deve ser um objeto.');
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
const unsupportedField = Object.keys(versionDeploy).find((field) => !ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS.has(field));
|
|
3520
|
+
if (unsupportedField) {
|
|
3521
|
+
throw new CliError(`Campo invalido: config.github.version_deploy.${unsupportedField}.`);
|
|
3522
|
+
}
|
|
3523
|
+
if (versionDeploy.enabled !== undefined && typeof versionDeploy.enabled !== 'boolean') {
|
|
3524
|
+
throw new CliError('config.github.version_deploy.enabled deve ser true ou false.');
|
|
3525
|
+
}
|
|
3526
|
+
if (versionDeploy.event !== undefined && !ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(versionDeploy.event)) {
|
|
3527
|
+
throw new CliError('config.github.version_deploy.event deve ser tag ou release.');
|
|
3528
|
+
}
|
|
3529
|
+
if (versionDeploy.tag_pattern !== undefined && !isValidGithubTagPattern(versionDeploy.tag_pattern)) {
|
|
3530
|
+
throw new CliError('Tag pattern invalido: use de 1 a 255 caracteres e evite caracteres de controle, [ ] { } e barra invertida.');
|
|
3531
|
+
}
|
|
3532
|
+
if (versionDeploy.include_prereleases !== undefined && typeof versionDeploy.include_prereleases !== 'boolean') {
|
|
3533
|
+
throw new CliError('config.github.version_deploy.include_prereleases deve ser true ou false.');
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
const enabled = versionDeploy.enabled === true;
|
|
3537
|
+
const event = versionDeploy.event || 'tag';
|
|
3538
|
+
if (enabled && versionDeploy.tag_pattern === undefined) {
|
|
3539
|
+
throw new CliError('config.github.version_deploy.tag_pattern e obrigatorio quando o version_deploy esta ativo.');
|
|
3540
|
+
}
|
|
3541
|
+
if (enabled && github.auto_deploy === true) {
|
|
3542
|
+
throw new CliError('github.auto_deploy e github.version_deploy nao podem ficar ativos ao mesmo tempo.');
|
|
3543
|
+
}
|
|
3544
|
+
if (versionDeploy.include_prereleases === true && event !== 'release') {
|
|
3545
|
+
throw new CliError('github.version_deploy.include_prereleases so pode ser true no modo release.');
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
|
|
2946
3549
|
function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
2947
3550
|
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2948
3551
|
const nextPlan = normalizePlan(plan);
|
|
@@ -2959,6 +3562,17 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
|
2959
3562
|
throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2960
3563
|
}
|
|
2961
3564
|
|
|
3565
|
+
const hasGenericGitConfig = config?.source !== undefined || config?.build !== undefined;
|
|
3566
|
+
if (hasGenericGitConfig && (!isRecord(config.source) || !isRecord(config.build))) {
|
|
3567
|
+
throw new CliError('config.source e config.build devem ser informados juntos para projetos com origem Git.');
|
|
3568
|
+
}
|
|
3569
|
+
if (hasGenericGitConfig && nextTypeProject !== 'http') {
|
|
3570
|
+
throw new CliError('config.source e config.build so podem ser usados em projetos HTTP.');
|
|
3571
|
+
}
|
|
3572
|
+
if (hasGenericGitConfig && config.github !== undefined) {
|
|
3573
|
+
throw new CliError('Use config.github ou config.source/config.build, sem misturar os formatos de origem Git.');
|
|
3574
|
+
}
|
|
3575
|
+
|
|
2962
3576
|
const nextConfig = { ...config, type_project: nextTypeProject };
|
|
2963
3577
|
if (nextTypeProject === 'valkey') {
|
|
2964
3578
|
return {
|
|
@@ -2969,6 +3583,7 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
|
2969
3583
|
}
|
|
2970
3584
|
|
|
2971
3585
|
if (nextTypeProject === 'http' && nextConfig.github) {
|
|
3586
|
+
validateGithubVersionDeployConfig(nextConfig.github);
|
|
2972
3587
|
const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
|
|
2973
3588
|
if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
|
|
2974
3589
|
throw new CliError(`Runtime invalido: "${nextConfig.github.runtime || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_RUNTIME_VALUES)}. Docs: ${DOCS_RUNTIME_URL}`);
|
|
@@ -3080,11 +3695,45 @@ async function buildHttpGithubConfig() {
|
|
|
3080
3695
|
options: GITHUB_RUNTIME_VERSIONS[runtime].map((value) => ({ value })),
|
|
3081
3696
|
});
|
|
3082
3697
|
|
|
3083
|
-
const
|
|
3084
|
-
label: '
|
|
3698
|
+
const deployMode = await promptWizardSelect({
|
|
3699
|
+
label: 'Modo de deploy GitHub',
|
|
3085
3700
|
docs: DOCS_RUNTIME_URL,
|
|
3086
|
-
examples: ['
|
|
3701
|
+
examples: ['manual', 'branch', 'tag', 'release'],
|
|
3702
|
+
options: [
|
|
3703
|
+
{ value: 'manual', description: 'Deploy iniciado manualmente.' },
|
|
3704
|
+
{ value: 'branch', description: 'Deploy quando a branch configurada for atualizada.' },
|
|
3705
|
+
{ value: 'tag', description: 'Deploy quando uma tag corresponder ao padrao.' },
|
|
3706
|
+
{ value: 'release', description: 'Deploy quando uma release corresponder ao padrao.' },
|
|
3707
|
+
],
|
|
3087
3708
|
});
|
|
3709
|
+
promptWizardSelect.ui.state.httpGithubDeployMode = deployMode;
|
|
3710
|
+
|
|
3711
|
+
let versionDeploy;
|
|
3712
|
+
if (deployMode === 'tag' || deployMode === 'release') {
|
|
3713
|
+
const tagPattern = await promptWizardText({
|
|
3714
|
+
label: 'Padrao de tags',
|
|
3715
|
+
required: true,
|
|
3716
|
+
examples: ['v*', 'release/?*'],
|
|
3717
|
+
docs: DOCS_RUNTIME_URL,
|
|
3718
|
+
validate: (value) => isValidGithubTagPattern(value)
|
|
3719
|
+
? null
|
|
3720
|
+
: 'use de 1 a 255 caracteres; somente * e ? sao aceitos como curingas',
|
|
3721
|
+
});
|
|
3722
|
+
const includePrereleases = deployMode === 'release'
|
|
3723
|
+
? await promptWizardBoolean({
|
|
3724
|
+
label: 'Incluir prereleases',
|
|
3725
|
+
docs: DOCS_RUNTIME_URL,
|
|
3726
|
+
examples: ['nao', 'sim'],
|
|
3727
|
+
})
|
|
3728
|
+
: false;
|
|
3729
|
+
|
|
3730
|
+
versionDeploy = {
|
|
3731
|
+
enabled: true,
|
|
3732
|
+
event: deployMode,
|
|
3733
|
+
tag_pattern: tagPattern,
|
|
3734
|
+
include_prereleases: includePrereleases,
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3088
3737
|
|
|
3089
3738
|
const start_command = await promptWizardText({
|
|
3090
3739
|
label: 'Start command',
|
|
@@ -3114,7 +3763,8 @@ async function buildHttpGithubConfig() {
|
|
|
3114
3763
|
branch,
|
|
3115
3764
|
runtime,
|
|
3116
3765
|
version,
|
|
3117
|
-
auto_deploy,
|
|
3766
|
+
auto_deploy: deployMode === 'branch',
|
|
3767
|
+
...(versionDeploy ? { version_deploy: versionDeploy } : {}),
|
|
3118
3768
|
start_command,
|
|
3119
3769
|
pre_build_command: preBuildCommand,
|
|
3120
3770
|
build_command: buildCommandRaw,
|
|
@@ -3621,6 +4271,12 @@ async function handleProjectCreate(session, flags) {
|
|
|
3621
4271
|
: undefined;
|
|
3622
4272
|
const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
|
|
3623
4273
|
|
|
4274
|
+
if (validated.config.source !== undefined || validated.config.build !== undefined) {
|
|
4275
|
+
if (!await supportsNeutralGitRoutes(session, flags, orgId)) {
|
|
4276
|
+
throw new CliError('Esta API nao oferece suporte a criacao de projetos com origem Git generica. Atualize a API ou use a configuracao GitHub legada compativel.');
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
|
|
3624
4280
|
const payload = await request(session, flags, 'POST', '/project', {
|
|
3625
4281
|
orgId,
|
|
3626
4282
|
headers: idempotencyKey === undefined ? {} : { 'Idempotency-Key': idempotencyKey },
|
|
@@ -3679,6 +4335,160 @@ async function handleProjectInfo(session, flags) {
|
|
|
3679
4335
|
printProject({ ...project, id: projectId });
|
|
3680
4336
|
}
|
|
3681
4337
|
|
|
4338
|
+
function githubConfigFromPayload(payload) {
|
|
4339
|
+
const github = isRecord(payload?.github)
|
|
4340
|
+
? payload.github
|
|
4341
|
+
: isRecord(payload?.config?.github) ? payload.config.github : payload;
|
|
4342
|
+
if (!isRecord(github) || !github.repository_owner || !github.repository_name) {
|
|
4343
|
+
throw new CliError('O projeto nao possui uma configuracao GitHub disponivel.');
|
|
4344
|
+
}
|
|
4345
|
+
return github;
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
function publicGithubConfiguration(github) {
|
|
4349
|
+
const output = Object.fromEntries(GITHUB_PUBLIC_CONFIG_FIELDS
|
|
4350
|
+
.filter((field) => github[field] !== undefined)
|
|
4351
|
+
.map((field) => [field, github[field]]));
|
|
4352
|
+
if (isRecord(github.version_deploy)) {
|
|
4353
|
+
output.version_deploy = Object.fromEntries(GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS
|
|
4354
|
+
.filter((field) => github.version_deploy[field] !== undefined)
|
|
4355
|
+
.map((field) => [field, github.version_deploy[field]]));
|
|
4356
|
+
}
|
|
4357
|
+
return output;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
function githubDeploymentMode(github) {
|
|
4361
|
+
const versionDeployEnabled = github.version_deploy?.enabled === true;
|
|
4362
|
+
if (github.auto_deploy === true && versionDeployEnabled) {
|
|
4363
|
+
throw new CliError('A configuracao GitHub do projeto possui modos de deploy conflitantes.');
|
|
4364
|
+
}
|
|
4365
|
+
if (github.auto_deploy === true) return 'branch';
|
|
4366
|
+
if (!versionDeployEnabled) return 'manual';
|
|
4367
|
+
if (!ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(github.version_deploy.event)) {
|
|
4368
|
+
throw new CliError('A configuracao GitHub do projeto possui um modo de deploy invalido.');
|
|
4369
|
+
}
|
|
4370
|
+
return github.version_deploy.event;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
function printGithubConfiguration(github) {
|
|
4374
|
+
const mode = githubDeploymentMode(github);
|
|
4375
|
+
const rows = [
|
|
4376
|
+
{ field: 'Repositorio', value: `${github.repository_owner}/${github.repository_name}` },
|
|
4377
|
+
{ field: 'Branch', value: github.branch || '-' },
|
|
4378
|
+
{ field: 'Runtime', value: github.runtime ? `${github.runtime}@${github.version || '-'}` : '-' },
|
|
4379
|
+
{ field: 'Modo de deploy', value: mode },
|
|
4380
|
+
];
|
|
4381
|
+
if (mode === 'tag' || mode === 'release') {
|
|
4382
|
+
rows.push(
|
|
4383
|
+
{ field: 'Padrao de tags', value: github.version_deploy.tag_pattern || '-' },
|
|
4384
|
+
{ field: 'Incluir prereleases', value: github.version_deploy.include_prereleases === true ? 'sim' : 'nao' },
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
printTable(rows, [
|
|
4388
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
4389
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
4390
|
+
]);
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
function parseGithubDeploySettings(flags) {
|
|
4394
|
+
const mode = String(flags.mode || '').toLowerCase();
|
|
4395
|
+
if (!['manual', 'branch', 'tag', 'release'].includes(mode)) {
|
|
4396
|
+
throw new CliError('Informe --mode manual, branch, tag ou release.');
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
const usesVersionDeploy = mode === 'tag' || mode === 'release';
|
|
4400
|
+
const hasTagPattern = flags.tagPattern !== undefined;
|
|
4401
|
+
const hasPrereleaseOption = flags.includePrereleases !== undefined;
|
|
4402
|
+
if (hasTagPattern && typeof flags.tagPattern !== 'string') {
|
|
4403
|
+
throw new CliError('--tag-pattern requires a value.');
|
|
4404
|
+
}
|
|
4405
|
+
if (hasPrereleaseOption && typeof flags.includePrereleases !== 'string') {
|
|
4406
|
+
throw new CliError('--include-prereleases requires a value of true or false.');
|
|
4407
|
+
}
|
|
4408
|
+
if (usesVersionDeploy && !hasTagPattern) {
|
|
4409
|
+
throw new CliError('--tag-pattern e obrigatorio para os modos tag e release.');
|
|
4410
|
+
}
|
|
4411
|
+
if (!usesVersionDeploy && hasTagPattern) {
|
|
4412
|
+
throw new CliError('--tag-pattern so pode ser usado nos modos tag e release.');
|
|
4413
|
+
}
|
|
4414
|
+
if (hasPrereleaseOption && mode !== 'release') {
|
|
4415
|
+
throw new CliError('--include-prereleases so pode ser usado com --mode release.');
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
let includePrereleases = false;
|
|
4419
|
+
if (hasPrereleaseOption) {
|
|
4420
|
+
const value = String(flags.includePrereleases).toLowerCase();
|
|
4421
|
+
if (value !== 'true' && value !== 'false') {
|
|
4422
|
+
throw new CliError('--include-prereleases deve ser true ou false.');
|
|
4423
|
+
}
|
|
4424
|
+
includePrereleases = value === 'true';
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
const tagPattern = usesVersionDeploy ? String(flags.tagPattern) : undefined;
|
|
4428
|
+
if (usesVersionDeploy && !isValidGithubTagPattern(tagPattern)) {
|
|
4429
|
+
throw new CliError('Tag pattern invalido: use de 1 a 255 caracteres e evite caracteres de controle, [ ] { } e barra invertida.');
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
return {
|
|
4433
|
+
mode,
|
|
4434
|
+
includePrereleases,
|
|
4435
|
+
body: {
|
|
4436
|
+
auto_deploy: mode === 'branch',
|
|
4437
|
+
version_deploy: usesVersionDeploy
|
|
4438
|
+
? { enabled: true, event: mode, tag_pattern: tagPattern, include_prereleases: includePrereleases }
|
|
4439
|
+
: { enabled: false },
|
|
4440
|
+
},
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function githubDeploySettingsMatch(github, desired) {
|
|
4445
|
+
let currentMode;
|
|
4446
|
+
try {
|
|
4447
|
+
currentMode = githubDeploymentMode(github);
|
|
4448
|
+
} catch {
|
|
4449
|
+
return false;
|
|
4450
|
+
}
|
|
4451
|
+
if (currentMode !== desired.mode) return false;
|
|
4452
|
+
if (desired.mode !== 'tag' && desired.mode !== 'release') return true;
|
|
4453
|
+
|
|
4454
|
+
const versionDeploy = github.version_deploy || {};
|
|
4455
|
+
return versionDeploy.enabled === true
|
|
4456
|
+
&& versionDeploy.event === desired.mode
|
|
4457
|
+
&& versionDeploy.tag_pattern === desired.body.version_deploy.tag_pattern
|
|
4458
|
+
&& (versionDeploy.include_prereleases === true) === desired.includePrereleases;
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
async function handleProjectGithub(session, flags) {
|
|
4462
|
+
const projectId = requireProjectId(flags, 'project github');
|
|
4463
|
+
if (!projectId) return;
|
|
4464
|
+
const orgId = await resolveOrgId(session, flags);
|
|
4465
|
+
const github = githubConfigFromPayload(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github`, { orgId })));
|
|
4466
|
+
const publicConfig = publicGithubConfiguration(github);
|
|
4467
|
+
|
|
4468
|
+
if (flags.json) return printJson(publicConfig);
|
|
4469
|
+
printGithubConfiguration(github);
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
async function handleProjectGithubDeploySettingsSet(session, flags) {
|
|
4473
|
+
const projectId = requireProjectId(flags, 'project github deploy-settings set');
|
|
4474
|
+
if (!projectId) return;
|
|
4475
|
+
const desired = parseGithubDeploySettings(flags);
|
|
4476
|
+
const orgId = await resolveOrgId(session, flags);
|
|
4477
|
+
|
|
4478
|
+
await request(session, flags, 'PATCH', `/project/${projectId}/github/deploy-settings`, {
|
|
4479
|
+
orgId,
|
|
4480
|
+
body: desired.body,
|
|
4481
|
+
});
|
|
4482
|
+
const github = githubConfigFromPayload(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github`, { orgId })));
|
|
4483
|
+
if (!githubDeploySettingsMatch(github, desired)) {
|
|
4484
|
+
throw new CliError('A configuracao solicitada nao foi confirmada pela API. Consulte "zenifra project github".');
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
const publicConfig = publicGithubConfiguration(github);
|
|
4488
|
+
if (flags.json) return printJson(publicConfig);
|
|
4489
|
+
process.stdout.write(`Configuracao de deploy GitHub confirmada: ${desired.mode}${desired.mode === 'tag' || desired.mode === 'release' ? ` (${desired.body.version_deploy.tag_pattern})` : ''}.\n`);
|
|
4490
|
+
}
|
|
4491
|
+
|
|
3682
4492
|
async function handleProjectLifecycleMutation(session, flags, action) {
|
|
3683
4493
|
const commandKey = `project ${action}`;
|
|
3684
4494
|
const projectId = requireProjectId(flags, commandKey);
|
|
@@ -4444,8 +5254,9 @@ async function handleDeployments(session, flags) {
|
|
|
4444
5254
|
return
|
|
4445
5255
|
}
|
|
4446
5256
|
const orgId = await resolveOrgId(session, flags);
|
|
5257
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
4447
5258
|
const query = buildQuery(flags, ['page', 'limit', 'branch', 'status']);
|
|
4448
|
-
const data = unwrapData(await request(session, flags, 'GET',
|
|
5259
|
+
const data = unwrapData(await request(session, flags, 'GET', `${projectApiPath}/builds${query}`, { orgId }));
|
|
4449
5260
|
const builds = asArray(data);
|
|
4450
5261
|
|
|
4451
5262
|
if (flags.json) return printJson(data);
|
|
@@ -4466,11 +5277,12 @@ async function handleDeploy(session, flags) {
|
|
|
4466
5277
|
return;
|
|
4467
5278
|
}
|
|
4468
5279
|
const orgId = await resolveOrgId(session, flags);
|
|
5280
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
4469
5281
|
const body = {};
|
|
4470
5282
|
if (flags.branch) body.branch = String(flags.branch);
|
|
4471
5283
|
if (flags.commitSha) body.commit_sha = String(flags.commitSha);
|
|
4472
5284
|
|
|
4473
|
-
const payload = await request(session, flags, 'POST',
|
|
5285
|
+
const payload = await request(session, flags, 'POST', `${projectApiPath}/deploy`, {
|
|
4474
5286
|
orgId,
|
|
4475
5287
|
body,
|
|
4476
5288
|
});
|
|
@@ -4480,9 +5292,9 @@ async function handleDeploy(session, flags) {
|
|
|
4480
5292
|
process.stdout.write(`Deploy iniciado${buildId ? `: ${buildId}` : '.'}\n`);
|
|
4481
5293
|
}
|
|
4482
5294
|
|
|
4483
|
-
async function getBuildLogs(session, flags,
|
|
5295
|
+
async function getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor = 0, limit = 200 } = {}) {
|
|
4484
5296
|
const query = buildQuery({ cursor, limit }, ['cursor', 'limit'])
|
|
4485
|
-
return unwrapData(await request(session, flags, 'GET',
|
|
5297
|
+
return unwrapData(await request(session, flags, 'GET', `${projectApiPath}/builds/${buildId}/logs${query}`, { orgId }))
|
|
4486
5298
|
}
|
|
4487
5299
|
|
|
4488
5300
|
function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
|
|
@@ -4533,7 +5345,7 @@ function printBuildLogEventsJson(logs) {
|
|
|
4533
5345
|
}
|
|
4534
5346
|
}
|
|
4535
5347
|
|
|
4536
|
-
async function followBuildLogs(session, flags,
|
|
5348
|
+
async function followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4537
5349
|
initialCursor = 0,
|
|
4538
5350
|
limit = 200,
|
|
4539
5351
|
intervalSeconds = 5,
|
|
@@ -4546,7 +5358,7 @@ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
|
4546
5358
|
const startedAt = Date.now()
|
|
4547
5359
|
|
|
4548
5360
|
while (true) {
|
|
4549
|
-
const result = await getBuildLogs(session, flags,
|
|
5361
|
+
const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4550
5362
|
cursor,
|
|
4551
5363
|
limit: pollLimit,
|
|
4552
5364
|
})
|
|
@@ -4587,20 +5399,27 @@ async function handleBuildLogs(session, flags) {
|
|
|
4587
5399
|
return
|
|
4588
5400
|
}
|
|
4589
5401
|
|
|
4590
|
-
const orgId = await resolveOrgId(session, flags);
|
|
4591
5402
|
const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
4592
5403
|
const limit = parseIntegerOption(flags.limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
5404
|
+
const intervalSeconds = flags.follow
|
|
5405
|
+
? parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 })
|
|
5406
|
+
: undefined;
|
|
5407
|
+
const timeoutSeconds = flags.follow
|
|
5408
|
+
? parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 })
|
|
5409
|
+
: undefined;
|
|
5410
|
+
const orgId = await resolveOrgId(session, flags);
|
|
5411
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
4593
5412
|
|
|
4594
5413
|
if (flags.follow) {
|
|
4595
|
-
return followBuildLogs(session, flags,
|
|
5414
|
+
return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4596
5415
|
initialCursor: cursor,
|
|
4597
5416
|
limit,
|
|
4598
|
-
intervalSeconds
|
|
4599
|
-
timeoutSeconds
|
|
5417
|
+
intervalSeconds,
|
|
5418
|
+
timeoutSeconds,
|
|
4600
5419
|
})
|
|
4601
5420
|
}
|
|
4602
5421
|
|
|
4603
|
-
const result = await getBuildLogs(session, flags,
|
|
5422
|
+
const result = await getBuildLogs(session, flags, projectApiPath, buildId, orgId, { cursor, limit })
|
|
4604
5423
|
if (flags.json) return printJson(result)
|
|
4605
5424
|
printBuildLogs(result?.logs)
|
|
4606
5425
|
}
|
|
@@ -4613,12 +5432,15 @@ async function handleDeployWatch(session, flags) {
|
|
|
4613
5432
|
return;
|
|
4614
5433
|
}
|
|
4615
5434
|
|
|
5435
|
+
const intervalSeconds = parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 });
|
|
5436
|
+
const timeoutSeconds = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 });
|
|
4616
5437
|
const orgId = await resolveOrgId(session, flags);
|
|
4617
|
-
|
|
5438
|
+
const projectApiPath = await resolveGitProjectApiPath(session, flags, projectId, orgId);
|
|
5439
|
+
return followBuildLogs(session, flags, projectApiPath, buildId, orgId, {
|
|
4618
5440
|
initialCursor: 0,
|
|
4619
5441
|
limit: 200,
|
|
4620
|
-
intervalSeconds
|
|
4621
|
-
timeoutSeconds
|
|
5442
|
+
intervalSeconds,
|
|
5443
|
+
timeoutSeconds,
|
|
4622
5444
|
})
|
|
4623
5445
|
}
|
|
4624
5446
|
|
|
@@ -4664,6 +5486,15 @@ async function main() {
|
|
|
4664
5486
|
if (command === 'login') return await handleLogin(session, flags);
|
|
4665
5487
|
if (command === 'logout') return handleLogout(session, flags);
|
|
4666
5488
|
if (command === 'plans') return handlePlans(session, flags);
|
|
5489
|
+
if (command === 'git' && subcommand === 'providers') return handleGitProviders(session, flags);
|
|
5490
|
+
if (command === 'git' && subcommand === 'runtimes') return handleGitRuntimes(session, flags);
|
|
5491
|
+
if (command === 'git' && subcommand === 'connections') return handleGitConnections(session, flags);
|
|
5492
|
+
if (command === 'git' && subcommand === 'repositories' && positional.length === 2) {
|
|
5493
|
+
process.stdout.write(commandHelp(['git', 'repositories']));
|
|
5494
|
+
return;
|
|
5495
|
+
}
|
|
5496
|
+
if (command === 'git' && subcommand === 'repositories' && positional[2] === 'resolve') return handleGitRepositoryResolve(session, flags);
|
|
5497
|
+
if (command === 'git' && subcommand === 'branches') return handleGitBranches(session, flags);
|
|
4667
5498
|
if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
|
|
4668
5499
|
if (command === 'orgs') return handleOrgs(session, flags);
|
|
4669
5500
|
if (command === 'whoami') return handleWhoami(session, flags);
|
|
@@ -4685,6 +5516,21 @@ async function main() {
|
|
|
4685
5516
|
if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'set') return handleProjectHealthcheckSet(session, flags);
|
|
4686
5517
|
if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
|
|
4687
5518
|
if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'failures') return handleProjectHealthcheckFailures(session, flags);
|
|
5519
|
+
if (command === 'project' && subcommand === 'github' && positional[2] === 'deploy-settings' && positional[3] === 'set') return handleProjectGithubDeploySettingsSet(session, flags);
|
|
5520
|
+
if (command === 'project' && subcommand === 'github' && positional[2] === 'deploy-settings') {
|
|
5521
|
+
process.stdout.write(commandHelp(['project', 'github', 'deploy-settings', 'set']));
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
5524
|
+
if (command === 'project' && subcommand === 'github' && positional.length === 2) return handleProjectGithub(session, flags);
|
|
5525
|
+
if (command === 'project' && subcommand === 'source' && positional.length === 2) return handleProjectSource(session, flags);
|
|
5526
|
+
if (command === 'project' && subcommand === 'source' && positional[2] === 'branches') return handleProjectSourceBranches(session, flags);
|
|
5527
|
+
if (command === 'project' && subcommand === 'source' && positional[2] === 'deploy-settings' && positional[3] === 'set') {
|
|
5528
|
+
return handleProjectSourceDeploySettingsSet(session, flags);
|
|
5529
|
+
}
|
|
5530
|
+
if (command === 'project' && subcommand === 'source' && positional[2] === 'deploy-settings') {
|
|
5531
|
+
process.stdout.write(commandHelp(['project', 'source', 'deploy-settings']));
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
4688
5534
|
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
4689
5535
|
if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
|
|
4690
5536
|
if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
|