@zenifra/cli 0.3.4 → 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
@@ -80,6 +80,10 @@ zenifra project metrics --project <project-id> --instance <instance-id>
80
80
  zenifra project metrics capabilities --project <project-id>
81
81
  zenifra project network --project <project-id> --view summary
82
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
83
87
  zenifra project envs --project <project-id>
84
88
  zenifra project env add --project <project-id> --name NODE_ENV --value production
85
89
  zenifra project env update --project <project-id> --name NODE_ENV --value staging
@@ -155,6 +159,22 @@ Fluxos:
155
159
 
156
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.
157
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
+
158
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.
159
179
 
160
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.
@@ -287,6 +307,8 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
287
307
 
288
308
  - `examples/http-project.json`: projeto HTTP com imagem OCI publica
289
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*`
290
312
  - `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
291
313
  - `examples/postgresql-project.json`: projeto PostgreSQL
292
314
  - `examples/mariadb-project.json`: projeto MariaDB
@@ -324,12 +346,15 @@ Valores aceitos:
324
346
  - `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
325
347
  - `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
326
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`
327
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
328
352
 
329
353
  Observacoes do wizard:
330
354
 
331
355
  - conflitos entre documentacao e API sao validados pelo contrato real aceito pela API
332
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
333
358
  - na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
334
359
  - em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
335
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',
@@ -114,6 +116,21 @@ const ALLOWED_PLAN_VALUES = new Set([
114
116
  const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
115
117
  const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey', 'clickhouse']);
116
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'];
117
134
  const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
118
135
  const GITHUB_RUNTIME_VERSIONS = {
119
136
  nodejs: ['24', '22', '20'],
@@ -205,6 +222,8 @@ Usage:
205
222
  zenifra project healthcheck failures --project <id> [--page <n>] [--limit <n>] [--json]
206
223
  zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
207
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]
208
227
  zenifra project exposure set --project <id> --exposure <public|private> [--json]
209
228
  zenifra project envs --project <id> [--json] [--show-values]
210
229
  zenifra project env add --project <id> --name <name> --value <value> [--json]
@@ -455,7 +474,41 @@ const HELP_SPECS = [
455
474
  description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
456
475
  examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
457
476
  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.'],
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
+ ],
459
512
  },
460
513
  {
461
514
  command: 'project info',
@@ -861,7 +914,9 @@ function parseArgs(argv) {
861
914
  continue;
862
915
  }
863
916
 
864
- 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);
865
920
  const key = rawKey.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
866
921
 
867
922
  if (inlineValue !== undefined) {
@@ -1218,7 +1273,9 @@ function estimateWizardTotal(state = {}) {
1218
1273
  total += 1;
1219
1274
 
1220
1275
  if (state.httpSource === 'github') {
1221
- total += 9;
1276
+ total += state.httpGithubDeployMode === 'release'
1277
+ ? 11
1278
+ : state.httpGithubDeployMode === 'tag' ? 10 : 9;
1222
1279
  } else if (state.httpSource === 'oci') {
1223
1280
  total += 2;
1224
1281
  if (state.httpImagePublic === false) {
@@ -2943,6 +3000,57 @@ function validateValkeyConfig(config, plan, catalog) {
2943
3000
  };
2944
3001
  }
2945
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
+
2946
3054
  function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2947
3055
  const nextTypeProject = normalizeTypeProject(config?.type_project);
2948
3056
  const nextPlan = normalizePlan(plan);
@@ -2969,6 +3077,7 @@ function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
2969
3077
  }
2970
3078
 
2971
3079
  if (nextTypeProject === 'http' && nextConfig.github) {
3080
+ validateGithubVersionDeployConfig(nextConfig.github);
2972
3081
  const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
2973
3082
  if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
2974
3083
  throw new CliError(`Runtime invalido: "${nextConfig.github.runtime || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_RUNTIME_VALUES)}. Docs: ${DOCS_RUNTIME_URL}`);
@@ -3080,11 +3189,45 @@ async function buildHttpGithubConfig() {
3080
3189
  options: GITHUB_RUNTIME_VERSIONS[runtime].map((value) => ({ value })),
3081
3190
  });
3082
3191
 
3083
- const auto_deploy = await promptWizardBoolean({
3084
- label: 'Auto deploy',
3192
+ const deployMode = await promptWizardSelect({
3193
+ label: 'Modo de deploy GitHub',
3085
3194
  docs: DOCS_RUNTIME_URL,
3086
- 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
+ ],
3087
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
+ }
3088
3231
 
3089
3232
  const start_command = await promptWizardText({
3090
3233
  label: 'Start command',
@@ -3114,7 +3257,8 @@ async function buildHttpGithubConfig() {
3114
3257
  branch,
3115
3258
  runtime,
3116
3259
  version,
3117
- auto_deploy,
3260
+ auto_deploy: deployMode === 'branch',
3261
+ ...(versionDeploy ? { version_deploy: versionDeploy } : {}),
3118
3262
  start_command,
3119
3263
  pre_build_command: preBuildCommand,
3120
3264
  build_command: buildCommandRaw,
@@ -3679,6 +3823,160 @@ async function handleProjectInfo(session, flags) {
3679
3823
  printProject({ ...project, id: projectId });
3680
3824
  }
3681
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
+
3682
3980
  async function handleProjectLifecycleMutation(session, flags, action) {
3683
3981
  const commandKey = `project ${action}`;
3684
3982
  const projectId = requireProjectId(flags, commandKey);
@@ -4685,6 +4983,12 @@ async function main() {
4685
4983
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'set') return handleProjectHealthcheckSet(session, flags);
4686
4984
  if (command === 'project' && subcommand === 'healthcheck' && positional[2] === 'disable') return handleProjectHealthcheckDisable(session, flags);
4687
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);
4688
4992
  if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
4689
4993
  if (command === 'project' && subcommand === 'stop') return handleProjectLifecycleMutation(session, flags, 'stop');
4690
4994
  if (command === 'project' && subcommand === 'resume') return handleProjectLifecycleMutation(session, flags, 'resume');
@@ -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.4",
3
+ "version": "0.4.0",
4
4
  "description": "Official Zenifra command line interface.",
5
5
  "type": "module",
6
6
  "license": "MIT",