@zenifra/cli 0.3.3 → 0.4.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 CHANGED
@@ -60,6 +60,7 @@ zenifra create project --name <name> --plan basic --payment-mode hourly --config
60
60
  zenifra create project --name <name> --plan premium --payment-mode hourly --config @examples/http-autoscaling-project.json
61
61
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
62
62
  zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
63
+ zenifra create project --name <name> --plan analytics-starter --payment-mode hourly --config @examples/clickhouse-project.json
63
64
  zenifra create project --name <name> --plan db-free --payment-mode hourly --config @examples/valkey-key-value-project.json
64
65
  zenifra create project --name <name> --plan cache-free --payment-mode hourly --config @examples/valkey-cache-project.json
65
66
  zenifra create project --name <name> --plan queue-free --payment-mode hourly --config @examples/valkey-queue-project.json
@@ -79,6 +80,10 @@ zenifra project metrics --project <project-id> --instance <instance-id>
79
80
  zenifra project metrics capabilities --project <project-id>
80
81
  zenifra project network --project <project-id> --view summary
81
82
  zenifra project image set --project <project-id> --image ghcr.io/zenifra/app:tag
83
+ zenifra project github --project <project-id>
84
+ zenifra project github deploy-settings set --project <project-id> --mode branch
85
+ zenifra project github deploy-settings set --project <project-id> --mode tag --tag-pattern "v*"
86
+ zenifra project github deploy-settings set --project <project-id> --mode release --tag-pattern "v*" --include-prereleases true
82
87
  zenifra project envs --project <project-id>
83
88
  zenifra project env add --project <project-id> --name NODE_ENV --value production
84
89
  zenifra project env update --project <project-id> --name NODE_ENV --value staging
@@ -154,6 +159,22 @@ Fluxos:
154
159
 
155
160
  Alguns builds antigos podem disponibilizar somente um resumo terminal. Nesse caso, a saida legivel avisa que eventos detalhados nao estavam disponiveis; `--json` preserva a resposta recebida da API.
156
161
 
162
+ ### Modos de deploy GitHub
163
+
164
+ Consulte a configuracao publica atual e escolha exatamente um modo de deploy:
165
+
166
+ ```bash
167
+ zenifra project github --project <project-id>
168
+ zenifra project github deploy-settings set --project <project-id> --mode manual
169
+ zenifra project github deploy-settings set --project <project-id> --mode branch
170
+ zenifra project github deploy-settings set --project <project-id> --mode tag --tag-pattern "v*"
171
+ zenifra project github deploy-settings set --project <project-id> --mode release --tag-pattern "v*" --include-prereleases false
172
+ ```
173
+
174
+ Os modos aceitos sao `manual`, `branch`, `tag` e `release`. `tag` e `release` exigem `--tag-pattern`, que aceita curingas `*` e `?`. O padrao de prereleases e `false`; a opcao so esta disponivel no modo `release`. A CLI le a configuracao salva de volta antes de confirmar uma alteracao. O comando `zenifra deploy --project <project-id> --branch <branch>` continua disponivel para iniciar um deploy manual.
175
+
176
+ Para criar um projeto HTTP com deploy por tag ou release, use `examples/http-github-tag-project.json` (padrao exato `v1.2.3`) ou `examples/http-github-release-project.json` (padrao `v*`).
177
+
157
178
  Antes de uma mutacao, confirme o perfil, a API e a organizacao ativa. Para criacoes, confirme tambem o catalogo, plano, pagamento, exposicao, dominio, origem do deploy, porta, instancias e ambientes. Depois, leia o projeto de volta e acompanhe o build/deployment ate um estado terminal.
158
179
 
159
180
  O dominio principal e um dominio personalizado sao campos diferentes. Nao repita o dominio principal em `config.custom_domains`; depois de adicionar um dominio personalizado, valide DNS/TLS e leia a URL final antes de considerar a operacao concluida.
@@ -286,9 +307,12 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
286
307
 
287
308
  - `examples/http-project.json`: projeto HTTP com imagem OCI publica
288
309
  - `examples/http-github-project.json`: projeto HTTP com build via GitHub
310
+ - `examples/http-github-tag-project.json`: projeto HTTP com deploy por tag e padrao exato
311
+ - `examples/http-github-release-project.json`: projeto HTTP com deploy por release e padrao `v*`
289
312
  - `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
290
313
  - `examples/postgresql-project.json`: projeto PostgreSQL
291
314
  - `examples/mariadb-project.json`: projeto MariaDB
315
+ - `examples/clickhouse-project.json`: projeto ClickHouse com Native TLS e armazenamento persistente
292
316
  - `examples/valkey-key-value-project.json`: projeto Valkey Key Value com armazenamento persistente
293
317
  - `examples/valkey-cache-project.json`: projeto Valkey Cache sem armazenamento persistente
294
318
  - `examples/valkey-queue-project.json`: projeto Valkey Queue com armazenamento persistente
@@ -306,6 +330,8 @@ O wizard atual cobre:
306
330
  - projetos `mariadb`
307
331
  - projetos `valkey` nos perfis `key_value`, `cache` e `queue`
308
332
 
333
+ Projetos `clickhouse` usam atualmente o fluxo nao interativo com `--config`, como em `examples/clickhouse-project.json`.
334
+
309
335
  `zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
310
336
  Configs HTTP nao interativas tambem devem informar `config.exposure`; use `public` para criar rota/dominio publico ou `private` para manter a aplicacao sem exposicao na internet.
311
337
  Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plans` para evitar suposicoes sobre custo.
@@ -313,19 +339,22 @@ Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plan
313
339
  Valores aceitos:
314
340
 
315
341
  - `payment_mode`: `hourly`, `monthly`, `yearly`
316
- - `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`
342
+ - `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`, `clickhouse`
317
343
  - `exposure` no `config` HTTP: `public`, `private`
318
- - `plan`: consulte `zenifra plans` para os planos atuais; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
344
+ - `plan`: consulte `zenifra plans` para os planos atuais; ClickHouse usa `analytics-*`; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
319
345
  - `config.profile` em projetos Valkey: `key_value`, `cache` ou `queue`
320
346
  - `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
321
347
  - `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
322
348
  - `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
349
+ - `config.github.auto_deploy`: use `true` para o modo `branch`; mantenha `false` ao habilitar `version_deploy`
350
+ - `config.github.version_deploy`: use `enabled: true`, `event: "tag"` ou `"release"` e um `tag_pattern` explicito; `include_prereleases` e opcional e padrao `false`
323
351
  - `config.autoscaling` (somente HTTP pago): `enabled: true`, `max_instances` maior ou igual a `config.instances` e alvos opcionais de CPU/memoria entre 1 e 100
324
352
 
325
353
  Observacoes do wizard:
326
354
 
327
355
  - conflitos entre documentacao e API sao validados pelo contrato real aceito pela API
328
356
  - o wizard oferece auto-scaling apenas quando o plano selecionado informa essa disponibilidade
357
+ - no modo GitHub, o wizard permite escolher entre `manual`, `branch`, `tag` e `release`; modos por versao exigem um padrao de tags
329
358
  - na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
330
359
  - em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
331
360
  - em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
package/bin/zenifra.mjs CHANGED
@@ -50,6 +50,7 @@ const KNOWN_FLAG_NAMES = new Set([
50
50
  'from',
51
51
  'help',
52
52
  'image',
53
+ 'includePrereleases',
53
54
  'idempotencyKey',
54
55
  'instance',
55
56
  'interval',
@@ -73,6 +74,7 @@ const KNOWN_FLAG_NAMES = new Set([
73
74
  'revoke',
74
75
  'showValues',
75
76
  'status',
77
+ 'tagPattern',
76
78
  'timeout',
77
79
  'to',
78
80
  'totp',
@@ -96,6 +98,10 @@ const ALLOWED_PLAN_VALUES = new Set([
96
98
  'db-basic',
97
99
  'db-premium',
98
100
  'db-enterprise',
101
+ 'analytics-sandbox',
102
+ 'analytics-starter',
103
+ 'analytics-production',
104
+ 'analytics-enterprise',
99
105
  'cache-free',
100
106
  'cache-starter',
101
107
  'cache-basic',
@@ -108,8 +114,23 @@ const ALLOWED_PLAN_VALUES = new Set([
108
114
  'queue-enterprise',
109
115
  ]);
110
116
  const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
111
- const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey']);
117
+ const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse']);
112
118
  const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
119
+ const ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS = new Set(['tag', 'release']);
120
+ const ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS = new Set(['enabled', 'event', 'tag_pattern', 'include_prereleases']);
121
+ const GITHUB_TAG_PATTERN_BLOCKED_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\[\]{}\\]/u;
122
+ const GITHUB_PUBLIC_CONFIG_FIELDS = [
123
+ 'repository_owner',
124
+ 'repository_name',
125
+ 'branch',
126
+ 'runtime',
127
+ 'version',
128
+ 'auto_deploy',
129
+ 'start_command',
130
+ 'pre_build_command',
131
+ 'build_command',
132
+ ];
133
+ const GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS = ['enabled', 'event', 'tag_pattern', 'include_prereleases'];
113
134
  const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
114
135
  const GITHUB_RUNTIME_VERSIONS = {
115
136
  nodejs: ['24', '22', '20'],
@@ -201,6 +222,8 @@ Usage:
201
222
  zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--json]
202
223
  zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
203
224
  zenifra project image set --project <id> --image <image> [--json]
225
+ zenifra project github --project <id> [--json]
226
+ zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]
204
227
  zenifra project exposure set --project <id> --exposure <public|private> [--json]
205
228
  zenifra project envs --project <id> [--json] [--show-values]
206
229
  zenifra project env add --project <id> --name <name> --value <value> [--json]
@@ -451,7 +474,41 @@ const HELP_SPECS = [
451
474
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
452
475
  examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
453
476
  output: 'Zenifra CLI - project',
454
- 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.'],
477
+ notes: ['Use "zenifra help project <subcomando>" para detalhes de info, stop, resume, delete, url, logs, metrics, capabilities, network, image, github, exposure, autoscaling, billing, envs e instances.'],
478
+ },
479
+ {
480
+ command: 'project github',
481
+ usage: 'zenifra project github --project <id> [--json]',
482
+ description: 'Mostra a configuracao GitHub publica e o modo atual de deploy do projeto.',
483
+ flags: ['--project <id> ID do projeto.', '--json Imprime a configuracao publica em JSON.'],
484
+ examples: ['zenifra project github --project proj_1', 'zenifra project github --project proj_1 --json'],
485
+ output: 'Repositorio: example-team/sample-app\nBranch: main\nRuntime: nodejs@24\nModo de deploy: release\nPadrao de tags: v*\nIncluir prereleases: nao',
486
+ 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}}',
487
+ notes: ['A resposta mostra apenas campos publicos da configuracao do projeto.'],
488
+ },
489
+ {
490
+ command: 'project github deploy-settings set',
491
+ usage: 'zenifra project github deploy-settings set --project <id> --mode <manual|branch|tag|release> [--tag-pattern <pattern>] [--include-prereleases <true|false>] [--json]',
492
+ description: 'Define um unico modo de deploy para o projeto com origem GitHub e confirma a configuracao salva.',
493
+ flags: [
494
+ '--project <id> ID do projeto.',
495
+ '--mode <manual|branch|tag|release> Modo de deploy exclusivo.',
496
+ '--tag-pattern <pattern> Padrao obrigatorio para tag e release. Aceita * e ?.',
497
+ '--include-prereleases <true|false> Inclui prereleases somente no modo release. Padrao: false.',
498
+ '--json Imprime a configuracao confirmada em JSON.',
499
+ ],
500
+ examples: [
501
+ 'zenifra project github deploy-settings set --project proj_1 --mode manual',
502
+ 'zenifra project github deploy-settings set --project proj_1 --mode branch',
503
+ 'zenifra project github deploy-settings set --project proj_1 --mode tag --tag-pattern "v*"',
504
+ 'zenifra project github deploy-settings set --project proj_1 --mode release --tag-pattern "v*" --include-prereleases true',
505
+ ],
506
+ output: 'Configuracao de deploy GitHub confirmada: release (v*).',
507
+ 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}}',
508
+ notes: [
509
+ 'Tag e release exigem --tag-pattern explicito; prereleases so podem ser habilitadas no modo release.',
510
+ 'A configuracao salva e lida novamente antes de a CLI informar sucesso. O comando deploy manual continua disponivel.',
511
+ ],
455
512
  },
456
513
  {
457
514
  command: 'project info',
@@ -857,7 +914,9 @@ function parseArgs(argv) {
857
914
  continue;
858
915
  }
859
916
 
860
- const [rawKey, inlineValue] = arg.slice(2).split('=', 2);
917
+ const argumentValueSeparator = arg.indexOf('=');
918
+ const rawKey = argumentValueSeparator === -1 ? arg.slice(2) : arg.slice(2, argumentValueSeparator);
919
+ const inlineValue = argumentValueSeparator === -1 ? undefined : arg.slice(argumentValueSeparator + 1);
861
920
  const key = rawKey.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
862
921
 
863
922
  if (inlineValue !== undefined) {
@@ -1214,7 +1273,9 @@ function estimateWizardTotal(state = {}) {
1214
1273
  total += 1;
1215
1274
 
1216
1275
  if (state.httpSource === 'github') {
1217
- total += 9;
1276
+ total += state.httpGithubDeployMode === 'release'
1277
+ ? 11
1278
+ : state.httpGithubDeployMode === 'tag' ? 10 : 9;
1218
1279
  } else if (state.httpSource === 'oci') {
1219
1280
  total += 2;
1220
1281
  if (state.httpImagePublic === false) {
@@ -2422,6 +2483,14 @@ function normalizePlan(value) {
2422
2483
  ['dbpremium', 'db-premium'],
2423
2484
  ['db_enterprise', 'db-enterprise'],
2424
2485
  ['dbenterprise', 'db-enterprise'],
2486
+ ['analytics_sandbox', 'analytics-sandbox'],
2487
+ ['analyticssandbox', 'analytics-sandbox'],
2488
+ ['analytics_starter', 'analytics-starter'],
2489
+ ['analyticsstarter', 'analytics-starter'],
2490
+ ['analytics_production', 'analytics-production'],
2491
+ ['analyticsproduction', 'analytics-production'],
2492
+ ['analytics_enterprise', 'analytics-enterprise'],
2493
+ ['analyticsenterprise', 'analytics-enterprise'],
2425
2494
  ['cache_free', 'cache-free'],
2426
2495
  ['cachefree', 'cache-free'],
2427
2496
  ['cache_starter', 'cache-starter'],
@@ -2481,6 +2550,8 @@ function normalizeTypeProject(value) {
2481
2550
  ['valkey', 'valkey'],
2482
2551
  ['key value', 'valkey'],
2483
2552
  ['key-value', 'valkey'],
2553
+ ['clickhouse', 'clickhouse'],
2554
+ ['click house', 'clickhouse'],
2484
2555
  ]);
2485
2556
  return aliases.get(raw) || raw;
2486
2557
  }
@@ -2929,6 +3000,57 @@ function validateValkeyConfig(config, plan, catalog) {
2929
3000
  };
2930
3001
  }
2931
3002
 
3003
+ function isValidGithubTagPattern(value) {
3004
+ if (typeof value !== 'string') return false;
3005
+ const length = Array.from(value).length;
3006
+ return length >= 1 && length <= 255 && value.trim().length > 0
3007
+ && !GITHUB_TAG_PATTERN_BLOCKED_CHARACTERS.test(value);
3008
+ }
3009
+
3010
+ function validateGithubVersionDeployConfig(github) {
3011
+ if (!isRecord(github)) {
3012
+ throw new CliError('config.github deve ser um objeto.');
3013
+ }
3014
+ if (github.auto_deploy !== undefined && typeof github.auto_deploy !== 'boolean') {
3015
+ throw new CliError('config.github.auto_deploy deve ser true ou false.');
3016
+ }
3017
+
3018
+ const versionDeploy = github.version_deploy;
3019
+ if (versionDeploy === undefined) return;
3020
+ if (!isRecord(versionDeploy)) {
3021
+ throw new CliError('config.github.version_deploy deve ser um objeto.');
3022
+ }
3023
+
3024
+ const unsupportedField = Object.keys(versionDeploy).find((field) => !ALLOWED_GITHUB_VERSION_DEPLOY_FIELDS.has(field));
3025
+ if (unsupportedField) {
3026
+ throw new CliError(`Campo invalido: config.github.version_deploy.${unsupportedField}.`);
3027
+ }
3028
+ if (versionDeploy.enabled !== undefined && typeof versionDeploy.enabled !== 'boolean') {
3029
+ throw new CliError('config.github.version_deploy.enabled deve ser true ou false.');
3030
+ }
3031
+ if (versionDeploy.event !== undefined && !ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(versionDeploy.event)) {
3032
+ throw new CliError('config.github.version_deploy.event deve ser tag ou release.');
3033
+ }
3034
+ if (versionDeploy.tag_pattern !== undefined && !isValidGithubTagPattern(versionDeploy.tag_pattern)) {
3035
+ throw new CliError('Tag pattern invalido: use de 1 a 255 caracteres e evite caracteres de controle, [ ] { } e barra invertida.');
3036
+ }
3037
+ if (versionDeploy.include_prereleases !== undefined && typeof versionDeploy.include_prereleases !== 'boolean') {
3038
+ throw new CliError('config.github.version_deploy.include_prereleases deve ser true ou false.');
3039
+ }
3040
+
3041
+ const enabled = versionDeploy.enabled === true;
3042
+ const event = versionDeploy.event || 'tag';
3043
+ if (enabled && versionDeploy.tag_pattern === undefined) {
3044
+ throw new CliError('config.github.version_deploy.tag_pattern e obrigatorio quando o version_deploy esta ativo.');
3045
+ }
3046
+ if (enabled && github.auto_deploy === true) {
3047
+ throw new CliError('github.auto_deploy e github.version_deploy nao podem ficar ativos ao mesmo tempo.');
3048
+ }
3049
+ if (versionDeploy.include_prereleases === true && event !== 'release') {
3050
+ throw new CliError('github.version_deploy.include_prereleases so pode ser true no modo release.');
3051
+ }
3052
+ }
3053
+
2932
3054
  function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2933
3055
  const nextTypeProject = normalizeTypeProject(config?.type_project);
2934
3056
  const nextPlan = normalizePlan(plan);
@@ -2955,6 +3077,7 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2955
3077
  }
2956
3078
 
2957
3079
  if (nextTypeProject === 'http' && nextConfig.github) {
3080
+ validateGithubVersionDeployConfig(nextConfig.github);
2958
3081
  const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
2959
3082
  if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
2960
3083
  throw new CliError(`Runtime invalido: "${nextConfig.github.runtime || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_RUNTIME_VALUES)}. Docs: ${DOCS_RUNTIME_URL}`);
@@ -3066,11 +3189,45 @@ async function buildHttpGithubConfig() {
3066
3189
  options: GITHUB_RUNTIME_VERSIONS[runtime].map((value) => ({ value })),
3067
3190
  });
3068
3191
 
3069
- const auto_deploy = await promptWizardBoolean({
3070
- label: 'Auto deploy',
3192
+ const deployMode = await promptWizardSelect({
3193
+ label: 'Modo de deploy GitHub',
3071
3194
  docs: DOCS_RUNTIME_URL,
3072
- examples: ['sim', 'nao'],
3195
+ examples: ['manual', 'branch', 'tag', 'release'],
3196
+ options: [
3197
+ { value: 'manual', description: 'Deploy iniciado manualmente.' },
3198
+ { value: 'branch', description: 'Deploy quando a branch configurada for atualizada.' },
3199
+ { value: 'tag', description: 'Deploy quando uma tag corresponder ao padrao.' },
3200
+ { value: 'release', description: 'Deploy quando uma release corresponder ao padrao.' },
3201
+ ],
3073
3202
  });
3203
+ promptWizardSelect.ui.state.httpGithubDeployMode = deployMode;
3204
+
3205
+ let versionDeploy;
3206
+ if (deployMode === 'tag' || deployMode === 'release') {
3207
+ const tagPattern = await promptWizardText({
3208
+ label: 'Padrao de tags',
3209
+ required: true,
3210
+ examples: ['v*', 'release/?*'],
3211
+ docs: DOCS_RUNTIME_URL,
3212
+ validate: (value) => isValidGithubTagPattern(value)
3213
+ ? null
3214
+ : 'use de 1 a 255 caracteres; somente * e ? sao aceitos como curingas',
3215
+ });
3216
+ const includePrereleases = deployMode === 'release'
3217
+ ? await promptWizardBoolean({
3218
+ label: 'Incluir prereleases',
3219
+ docs: DOCS_RUNTIME_URL,
3220
+ examples: ['nao', 'sim'],
3221
+ })
3222
+ : false;
3223
+
3224
+ versionDeploy = {
3225
+ enabled: true,
3226
+ event: deployMode,
3227
+ tag_pattern: tagPattern,
3228
+ include_prereleases: includePrereleases,
3229
+ };
3230
+ }
3074
3231
 
3075
3232
  const start_command = await promptWizardText({
3076
3233
  label: 'Start command',
@@ -3100,7 +3257,8 @@ async function buildHttpGithubConfig() {
3100
3257
  branch,
3101
3258
  runtime,
3102
3259
  version,
3103
- auto_deploy,
3260
+ auto_deploy: deployMode === 'branch',
3261
+ ...(versionDeploy ? { version_deploy: versionDeploy } : {}),
3104
3262
  start_command,
3105
3263
  pre_build_command: preBuildCommand,
3106
3264
  build_command: buildCommandRaw,
@@ -3665,6 +3823,160 @@ async function handleProjectInfo(session, flags) {
3665
3823
  printProject({ ...project, id: projectId });
3666
3824
  }
3667
3825
 
3826
+ function githubConfigFromPayload(payload) {
3827
+ const github = isRecord(payload?.github)
3828
+ ? payload.github
3829
+ : isRecord(payload?.config?.github) ? payload.config.github : payload;
3830
+ if (!isRecord(github) || !github.repository_owner || !github.repository_name) {
3831
+ throw new CliError('O projeto nao possui uma configuracao GitHub disponivel.');
3832
+ }
3833
+ return github;
3834
+ }
3835
+
3836
+ function publicGithubConfiguration(github) {
3837
+ const output = Object.fromEntries(GITHUB_PUBLIC_CONFIG_FIELDS
3838
+ .filter((field) => github[field] !== undefined)
3839
+ .map((field) => [field, github[field]]));
3840
+ if (isRecord(github.version_deploy)) {
3841
+ output.version_deploy = Object.fromEntries(GITHUB_PUBLIC_VERSION_DEPLOY_FIELDS
3842
+ .filter((field) => github.version_deploy[field] !== undefined)
3843
+ .map((field) => [field, github.version_deploy[field]]));
3844
+ }
3845
+ return output;
3846
+ }
3847
+
3848
+ function githubDeploymentMode(github) {
3849
+ const versionDeployEnabled = github.version_deploy?.enabled === true;
3850
+ if (github.auto_deploy === true && versionDeployEnabled) {
3851
+ throw new CliError('A configuracao GitHub do projeto possui modos de deploy conflitantes.');
3852
+ }
3853
+ if (github.auto_deploy === true) return 'branch';
3854
+ if (!versionDeployEnabled) return 'manual';
3855
+ if (!ALLOWED_GITHUB_VERSION_DEPLOY_EVENTS.has(github.version_deploy.event)) {
3856
+ throw new CliError('A configuracao GitHub do projeto possui um modo de deploy invalido.');
3857
+ }
3858
+ return github.version_deploy.event;
3859
+ }
3860
+
3861
+ function printGithubConfiguration(github) {
3862
+ const mode = githubDeploymentMode(github);
3863
+ const rows = [
3864
+ { field: 'Repositorio', value: `${github.repository_owner}/${github.repository_name}` },
3865
+ { field: 'Branch', value: github.branch || '-' },
3866
+ { field: 'Runtime', value: github.runtime ? `${github.runtime}@${github.version || '-'}` : '-' },
3867
+ { field: 'Modo de deploy', value: mode },
3868
+ ];
3869
+ if (mode === 'tag' || mode === 'release') {
3870
+ rows.push(
3871
+ { field: 'Padrao de tags', value: github.version_deploy.tag_pattern || '-' },
3872
+ { field: 'Incluir prereleases', value: github.version_deploy.include_prereleases === true ? 'sim' : 'nao' },
3873
+ );
3874
+ }
3875
+ printTable(rows, [
3876
+ { label: 'Campo', value: (row) => row.field },
3877
+ { label: 'Valor', value: (row) => row.value },
3878
+ ]);
3879
+ }
3880
+
3881
+ function parseGithubDeploySettings(flags) {
3882
+ const mode = String(flags.mode || '').toLowerCase();
3883
+ if (!['manual', 'branch', 'tag', 'release'].includes(mode)) {
3884
+ throw new CliError('Informe --mode manual, branch, tag ou release.');
3885
+ }
3886
+
3887
+ const usesVersionDeploy = mode === 'tag' || mode === 'release';
3888
+ const hasTagPattern = flags.tagPattern !== undefined;
3889
+ const hasPrereleaseOption = flags.includePrereleases !== undefined;
3890
+ if (hasTagPattern && typeof flags.tagPattern !== 'string') {
3891
+ throw new CliError('--tag-pattern requires a value.');
3892
+ }
3893
+ if (hasPrereleaseOption && typeof flags.includePrereleases !== 'string') {
3894
+ throw new CliError('--include-prereleases requires a value of true or false.');
3895
+ }
3896
+ if (usesVersionDeploy && !hasTagPattern) {
3897
+ throw new CliError('--tag-pattern e obrigatorio para os modos tag e release.');
3898
+ }
3899
+ if (!usesVersionDeploy && hasTagPattern) {
3900
+ throw new CliError('--tag-pattern so pode ser usado nos modos tag e release.');
3901
+ }
3902
+ if (hasPrereleaseOption && mode !== 'release') {
3903
+ throw new CliError('--include-prereleases so pode ser usado com --mode release.');
3904
+ }
3905
+
3906
+ let includePrereleases = false;
3907
+ if (hasPrereleaseOption) {
3908
+ const value = String(flags.includePrereleases).toLowerCase();
3909
+ if (value !== 'true' && value !== 'false') {
3910
+ throw new CliError('--include-prereleases deve ser true ou false.');
3911
+ }
3912
+ includePrereleases = value === 'true';
3913
+ }
3914
+
3915
+ const tagPattern = usesVersionDeploy ? String(flags.tagPattern) : undefined;
3916
+ if (usesVersionDeploy && !isValidGithubTagPattern(tagPattern)) {
3917
+ throw new CliError('Tag pattern invalido: use de 1 a 255 caracteres e evite caracteres de controle, [ ] { } e barra invertida.');
3918
+ }
3919
+
3920
+ return {
3921
+ mode,
3922
+ includePrereleases,
3923
+ body: {
3924
+ auto_deploy: mode === 'branch',
3925
+ version_deploy: usesVersionDeploy
3926
+ ? { enabled: true, event: mode, tag_pattern: tagPattern, include_prereleases: includePrereleases }
3927
+ : { enabled: false },
3928
+ },
3929
+ };
3930
+ }
3931
+
3932
+ function githubDeploySettingsMatch(github, desired) {
3933
+ let currentMode;
3934
+ try {
3935
+ currentMode = githubDeploymentMode(github);
3936
+ } catch {
3937
+ return false;
3938
+ }
3939
+ if (currentMode !== desired.mode) return false;
3940
+ if (desired.mode !== 'tag' && desired.mode !== 'release') return true;
3941
+
3942
+ const versionDeploy = github.version_deploy || {};
3943
+ return versionDeploy.enabled === true
3944
+ && versionDeploy.event === desired.mode
3945
+ && versionDeploy.tag_pattern === desired.body.version_deploy.tag_pattern
3946
+ && (versionDeploy.include_prereleases === true) === desired.includePrereleases;
3947
+ }
3948
+
3949
+ async function handleProjectGithub(session, flags) {
3950
+ const projectId = requireProjectId(flags, 'project github');
3951
+ if (!projectId) return;
3952
+ const orgId = await resolveOrgId(session, flags);
3953
+ const github = githubConfigFromPayload(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github`, { orgId })));
3954
+ const publicConfig = publicGithubConfiguration(github);
3955
+
3956
+ if (flags.json) return printJson(publicConfig);
3957
+ printGithubConfiguration(github);
3958
+ }
3959
+
3960
+ async function handleProjectGithubDeploySettingsSet(session, flags) {
3961
+ const projectId = requireProjectId(flags, 'project github deploy-settings set');
3962
+ if (!projectId) return;
3963
+ const desired = parseGithubDeploySettings(flags);
3964
+ const orgId = await resolveOrgId(session, flags);
3965
+
3966
+ await request(session, flags, 'PATCH', `/project/${projectId}/github/deploy-settings`, {
3967
+ orgId,
3968
+ body: desired.body,
3969
+ });
3970
+ const github = githubConfigFromPayload(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github`, { orgId })));
3971
+ if (!githubDeploySettingsMatch(github, desired)) {
3972
+ throw new CliError('A configuracao solicitada nao foi confirmada pela API. Consulte "zenifra project github".');
3973
+ }
3974
+
3975
+ const publicConfig = publicGithubConfiguration(github);
3976
+ if (flags.json) return printJson(publicConfig);
3977
+ process.stdout.write(`Configuracao de deploy GitHub confirmada: ${desired.mode}${desired.mode === 'tag' || desired.mode === 'release' ? ` (${desired.body.version_deploy.tag_pattern})` : ''}.\n`);
3978
+ }
3979
+
3668
3980
  async function handleProjectLifecycleMutation(session, flags, action) {
3669
3981
  const commandKey = `project ${action}`;
3670
3982
  const projectId = requireProjectId(flags, commandKey);
@@ -4671,6 +4983,12 @@ async function main() {
4671
4983
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'set') return handleProjectHealthcheckSet(session, flags);
4672
4984
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
4673
4985
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'failures') return handleProjectHealthcheckFailures(session, flags);
4986
+ if (command === 'project' && subcommand === 'github' && positional[2] === 'deploy-settings' && positional[3] === 'set') return handleProjectGithubDeploySettingsSet(session, flags);
4987
+ if (command === 'project' && subcommand === 'github' && positional[2] === 'deploy-settings') {
4988
+ process.stdout.write(commandHelp(['project', 'github', 'deploy-settings', 'set']));
4989
+ return;
4990
+ }
4991
+ if (command === 'project' && subcommand === 'github' && positional.length === 2) return handleProjectGithub(session, flags);
4674
4992
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4675
4993
  if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
4676
4994
  if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
@@ -0,0 +1,19 @@
1
+ {
2
+ "type_project": "clickhouse",
3
+ "version": "26.8.6.5",
4
+ "instances": 1,
5
+ "storage": {
6
+ "capacity": 20,
7
+ "persistent": true
8
+ },
9
+ "envs": [],
10
+ "network_access": {
11
+ "ingress_white_list": [
12
+ {
13
+ "cidr": "203.0.113.10/32",
14
+ "description": "Application egress IP"
15
+ }
16
+ ],
17
+ "ingress_black_list": []
18
+ }
19
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "type_project": "http",
3
+ "exposure": "public",
4
+ "github": {
5
+ "repository_owner": "example-team",
6
+ "repository_name": "sample-app",
7
+ "branch": "main",
8
+ "runtime": "nodejs",
9
+ "version": "24",
10
+ "auto_deploy": false,
11
+ "version_deploy": {
12
+ "enabled": true,
13
+ "event": "release",
14
+ "tag_pattern": "v*",
15
+ "include_prereleases": false
16
+ },
17
+ "start_command": "npm start",
18
+ "pre_build_command": null,
19
+ "build_command": "npm run build"
20
+ },
21
+ "port": 3000,
22
+ "instances": 1,
23
+ "storage": {
24
+ "persistent": false,
25
+ "capacity": 1
26
+ },
27
+ "envs": [
28
+ {
29
+ "name": "NODE_ENV",
30
+ "value": "production"
31
+ }
32
+ ],
33
+ "network_access": {
34
+ "ingress_white_list": [
35
+ {
36
+ "cidr": "0.0.0.0/0",
37
+ "description": "Allow all"
38
+ }
39
+ ],
40
+ "ingress_black_list": []
41
+ }
42
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "type_project": "http",
3
+ "exposure": "public",
4
+ "github": {
5
+ "repository_owner": "example-team",
6
+ "repository_name": "sample-app",
7
+ "branch": "main",
8
+ "runtime": "nodejs",
9
+ "version": "24",
10
+ "auto_deploy": false,
11
+ "version_deploy": {
12
+ "enabled": true,
13
+ "event": "tag",
14
+ "tag_pattern": "v1.2.3",
15
+ "include_prereleases": false
16
+ },
17
+ "start_command": "npm start",
18
+ "pre_build_command": null,
19
+ "build_command": "npm run build"
20
+ },
21
+ "port": 3000,
22
+ "instances": 1,
23
+ "storage": {
24
+ "persistent": false,
25
+ "capacity": 1
26
+ },
27
+ "envs": [
28
+ {
29
+ "name": "NODE_ENV",
30
+ "value": "production"
31
+ }
32
+ ],
33
+ "network_access": {
34
+ "ingress_white_list": [
35
+ {
36
+ "cidr": "0.0.0.0/0",
37
+ "description": "Allow all"
38
+ }
39
+ ],
40
+ "ingress_black_list": []
41
+ }
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenifra/cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",