@neetru/cli 2.29.0 → 2.32.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/CHANGELOG.md +141 -0
- package/README.md +63 -17
- package/dist/cli-kit/choices.d.ts +33 -0
- package/dist/cli-kit/choices.js +32 -0
- package/dist/cli-kit/command-context.d.ts +98 -0
- package/dist/cli-kit/command-context.js +108 -0
- package/dist/cli-kit/errors.d.ts +79 -0
- package/dist/cli-kit/errors.js +94 -0
- package/dist/cli-kit/index.d.ts +8 -0
- package/dist/cli-kit/index.js +8 -0
- package/dist/cli-kit/suggest.d.ts +74 -0
- package/dist/cli-kit/suggest.js +207 -0
- package/dist/commands/admin/oidc-login.js +1 -1
- package/dist/commands/admin/sdk-credential.js +1 -1
- package/dist/commands/autocomplete.d.ts +1 -1
- package/dist/commands/autocomplete.js +1 -0
- package/dist/commands/bug.d.ts +59 -0
- package/dist/commands/bug.js +114 -2
- package/dist/commands/build.d.ts +24 -1
- package/dist/commands/build.js +195 -73
- package/dist/commands/chat.d.ts +94 -1
- package/dist/commands/chat.js +258 -26
- package/dist/commands/customers.js +1 -1
- package/dist/commands/db-move.js +3 -3
- package/dist/commands/db.js +17 -24
- package/dist/commands/deploy.d.ts +136 -2
- package/dist/commands/deploy.js +547 -69
- package/dist/commands/deployments.js +11 -6
- package/dist/commands/dev-context.js +15 -5
- package/dist/commands/dev-up.d.ts +7 -5
- package/dist/commands/dev-up.js +24 -23
- package/dist/commands/docs-produto.d.ts +90 -0
- package/dist/commands/docs-produto.js +384 -0
- package/dist/commands/doctor.js +36 -1
- package/dist/commands/domains.js +85 -4
- package/dist/commands/env.d.ts +9 -3
- package/dist/commands/env.js +23 -3
- package/dist/commands/infra-read.js +2 -2
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +14 -1
- package/dist/commands/login.js +35 -8
- package/dist/commands/logs.js +12 -4
- package/dist/commands/mobile.js +33 -9
- package/dist/commands/new.d.ts +31 -1
- package/dist/commands/new.js +94 -4
- package/dist/commands/open.js +16 -6
- package/dist/commands/ops.js +1 -1
- package/dist/commands/products-db.d.ts +1 -0
- package/dist/commands/products-db.js +41 -8
- package/dist/commands/products.d.ts +17 -3
- package/dist/commands/products.js +84 -16
- package/dist/commands/promote.js +30 -5
- package/dist/commands/secrets.d.ts +91 -0
- package/dist/commands/secrets.js +471 -0
- package/dist/commands/servers.js +6 -4
- package/dist/commands/status.js +2 -2
- package/dist/commands/support.js +5 -3
- package/dist/commands/tenants.js +28 -16
- package/dist/commands/upgrade.d.ts +9 -0
- package/dist/commands/upgrade.js +50 -1
- package/dist/commands/vm-memory.js +1 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +9 -3
- package/dist/commands/workspaces.js +42 -11
- package/dist/index.js +142 -20
- package/dist/lib/api-client.d.ts +22 -1
- package/dist/lib/api-client.js +79 -8
- package/dist/lib/api-error.d.ts +14 -0
- package/dist/lib/api-error.js +107 -46
- package/dist/lib/build-targets.d.ts +127 -0
- package/dist/lib/build-targets.js +348 -0
- package/dist/lib/cli-error.d.ts +13 -0
- package/dist/lib/cli-error.js +24 -0
- package/dist/lib/cli-write.d.ts +43 -9
- package/dist/lib/cli-write.js +154 -45
- package/dist/lib/compose-generator.d.ts +13 -9
- package/dist/lib/compose-generator.js +28 -60
- package/dist/lib/config-schema.d.ts +36 -2
- package/dist/lib/config-schema.js +11 -0
- package/dist/lib/db-local/db-json.d.ts +1 -1
- package/dist/lib/db-local/db-json.js +13 -5
- package/dist/lib/db-local/env.d.ts +15 -3
- package/dist/lib/db-local/env.js +37 -10
- package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
- package/dist/lib/db-pipeline/build-deps.js +23 -3
- package/dist/lib/db-pipeline/types.d.ts +6 -2
- package/dist/lib/db-pipeline/types.js +0 -18
- package/dist/lib/deploy-target-contract.d.ts +152 -0
- package/dist/lib/deploy-target-contract.js +411 -0
- package/dist/lib/env-file-do-build.d.ts +19 -0
- package/dist/lib/env-file-do-build.js +44 -0
- package/dist/lib/envs.d.ts +108 -0
- package/dist/lib/envs.js +127 -0
- package/dist/lib/fs-seguro.d.ts +63 -0
- package/dist/lib/fs-seguro.js +108 -0
- package/dist/lib/mobile-local/mobile-json.js +10 -4
- package/dist/lib/pickers.d.ts +44 -7
- package/dist/lib/pickers.js +60 -4
- package/dist/lib/product-docs/check.d.ts +62 -0
- package/dist/lib/product-docs/check.js +489 -0
- package/dist/lib/product-docs/config.d.ts +66 -0
- package/dist/lib/product-docs/config.js +234 -0
- package/dist/lib/product-docs/dates.d.ts +17 -0
- package/dist/lib/product-docs/dates.js +41 -0
- package/dist/lib/product-docs/frontmatter.d.ts +43 -0
- package/dist/lib/product-docs/frontmatter.js +302 -0
- package/dist/lib/product-docs/git.d.ts +16 -0
- package/dist/lib/product-docs/git.js +92 -0
- package/dist/lib/product-docs/markdown.d.ts +27 -0
- package/dist/lib/product-docs/markdown.js +98 -0
- package/dist/lib/product-docs/ops.d.ts +133 -0
- package/dist/lib/product-docs/ops.js +975 -0
- package/dist/lib/product-docs/structure.d.ts +61 -0
- package/dist/lib/product-docs/structure.js +239 -0
- package/dist/lib/product-docs/sync.d.ts +107 -0
- package/dist/lib/product-docs/sync.js +322 -0
- package/dist/lib/product-docs/taxonomy.d.ts +46 -0
- package/dist/lib/product-docs/taxonomy.js +163 -0
- package/dist/lib/product-docs/templates.d.ts +53 -0
- package/dist/lib/product-docs/templates.js +245 -0
- package/dist/lib/public-secret-scan.d.ts +117 -0
- package/dist/lib/public-secret-scan.js +771 -0
- package/dist/lib/render.d.ts +16 -0
- package/dist/lib/render.js +34 -4
- package/dist/lib/segredo-produto.d.ts +124 -0
- package/dist/lib/segredo-produto.js +336 -0
- package/dist/lib/texto-utf8.d.ts +96 -0
- package/dist/lib/texto-utf8.js +196 -0
- package/package.json +2 -1
- package/dist/cli-kit/format.js.map +0 -1
- package/dist/cli-kit/glyphs.js.map +0 -1
- package/dist/cli-kit/index.js.map +0 -1
- package/dist/cli-kit/palette.js.map +0 -1
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/admin/iam-check.js.map +0 -1
- package/dist/commands/admin/oidc-login.js.map +0 -1
- package/dist/commands/admin/sdk-credential.js.map +0 -1
- package/dist/commands/agent-release.js.map +0 -1
- package/dist/commands/agent-write.js.map +0 -1
- package/dist/commands/ai-models.js.map +0 -1
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/api-catalog.js.map +0 -1
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/artifact-registry.js.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/autocomplete.js.map +0 -1
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/bootstrap.js.map +0 -1
- package/dist/commands/bug.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/chat-menu.js.map +0 -1
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/cloud-run.js.map +0 -1
- package/dist/commands/code.js.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/customers.js.map +0 -1
- package/dist/commands/db-move.js.map +0 -1
- package/dist/commands/db.js.map +0 -1
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/deployments.js.map +0 -1
- package/dist/commands/dev-context.js.map +0 -1
- package/dist/commands/dev-up.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/docs.js.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/domains.js.map +0 -1
- package/dist/commands/dr.js.map +0 -1
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/hosting-write.js.map +0 -1
- package/dist/commands/infra-read.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/mobile.js.map +0 -1
- package/dist/commands/mocks.js.map +0 -1
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/open.js.map +0 -1
- package/dist/commands/ops.js.map +0 -1
- package/dist/commands/products-db.js.map +0 -1
- package/dist/commands/products.js.map +0 -1
- package/dist/commands/promote.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/servers.js.map +0 -1
- package/dist/commands/smoke.js.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/support.js.map +0 -1
- package/dist/commands/surface-status.js.map +0 -1
- package/dist/commands/tenants.js.map +0 -1
- package/dist/commands/ui.js.map +0 -1
- package/dist/commands/upgrade.js.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/vm-memory.js.map +0 -1
- package/dist/commands/vm.js.map +0 -1
- package/dist/commands/webhooks.js.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/commands/workspaces.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-artifact-url.js.map +0 -1
- package/dist/lib/agent-commands.js.map +0 -1
- package/dist/lib/ai/auth-discovery.js.map +0 -1
- package/dist/lib/ai/context.js.map +0 -1
- package/dist/lib/ai/orchestrator.js.map +0 -1
- package/dist/lib/api-client.js.map +0 -1
- package/dist/lib/api-error.js.map +0 -1
- package/dist/lib/auth.js.map +0 -1
- package/dist/lib/cli-read.js.map +0 -1
- package/dist/lib/cli-write.js.map +0 -1
- package/dist/lib/code-agent-budget.js.map +0 -1
- package/dist/lib/code-agent-files.js.map +0 -1
- package/dist/lib/code-agent-gateway.js.map +0 -1
- package/dist/lib/code-agent-git-tools.js.map +0 -1
- package/dist/lib/code-agent-protocol.js.map +0 -1
- package/dist/lib/code-agent-session.js.map +0 -1
- package/dist/lib/code-agent-settings.js.map +0 -1
- package/dist/lib/code-agent-skills.js.map +0 -1
- package/dist/lib/code-agent-subagents.js.map +0 -1
- package/dist/lib/code-agent-tools.js.map +0 -1
- package/dist/lib/code-agent-tui.js.map +0 -1
- package/dist/lib/code-agent-types.js.map +0 -1
- package/dist/lib/code-agent-ui.js.map +0 -1
- package/dist/lib/code-agent-web-tools.js.map +0 -1
- package/dist/lib/code-agent-write-tools.js.map +0 -1
- package/dist/lib/compose-generator.js.map +0 -1
- package/dist/lib/config-schema.js.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/db-local/db-json.js.map +0 -1
- package/dist/lib/db-local/env.js.map +0 -1
- package/dist/lib/db-local/fingerprint.js.map +0 -1
- package/dist/lib/db-local/index.js.map +0 -1
- package/dist/lib/db-pipeline/build-deps.js.map +0 -1
- package/dist/lib/db-pipeline/errors.js.map +0 -1
- package/dist/lib/db-pipeline/index.js.map +0 -1
- package/dist/lib/db-pipeline/pipeline.js.map +0 -1
- package/dist/lib/db-pipeline/rehearse.js.map +0 -1
- package/dist/lib/db-pipeline/types.js.map +0 -1
- package/dist/lib/dev-local-context.js.map +0 -1
- package/dist/lib/iam-preflight.js.map +0 -1
- package/dist/lib/mobile-local/index.js.map +0 -1
- package/dist/lib/mobile-local/mobile-json.js.map +0 -1
- package/dist/lib/pickers.js.map +0 -1
- package/dist/lib/product-config.js.map +0 -1
- package/dist/lib/render.js.map +0 -1
- package/dist/lib/schema-cache.js.map +0 -1
- package/dist/lib/sse.js.map +0 -1
- package/dist/lib/telemetry.js.map +0 -1
- package/dist/lib/token-resolver.js.map +0 -1
- package/dist/lib/url-allowlist.js.map +0 -1
- package/dist/lib/version-check.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/logo.js.map +0 -1
- package/dist/utils/safe-exit.js.map +0 -1
- package/dist/utils/spinner-registry.js.map +0 -1
- package/dist/version.js.map +0 -1
- package/templates/dev/Dockerfile.agent +0 -39
- package/templates/dev/agent-entrypoint.sh +0 -41
- package/templates/dev/docker-compose.dev.yml +0 -63
package/dist/commands/deploy.js
CHANGED
|
@@ -27,14 +27,21 @@ import ora from 'ora';
|
|
|
27
27
|
import { log } from '../utils/logger.js';
|
|
28
28
|
import { apiRequest, CliNetworkError } from '../lib/api-client.js';
|
|
29
29
|
import { handleApiError } from '../lib/api-error.js';
|
|
30
|
+
import { exitWithCliError } from '../lib/cli-error.js';
|
|
31
|
+
import { invalidChoiceError } from '../cli-kit/choices.js';
|
|
32
|
+
import { DEPLOY_ENVIRONMENT_IDS, deployEnvParaRuntime, } from '../lib/envs.js';
|
|
30
33
|
import { resolveStepUp } from '../lib/cli-write.js';
|
|
31
34
|
import { runBuild } from './build.js';
|
|
32
35
|
import { formatDeployConsole, isDeployUnverified, deployStatusLabel } from './deployments.js';
|
|
33
36
|
import { readDbJson, schemaFingerprint } from '../lib/db-local/index.js';
|
|
34
37
|
// bug_863ae5: pre-flight IAM check.
|
|
35
38
|
import { assertIamReady } from '../lib/iam-preflight.js';
|
|
39
|
+
import { ALLOW_PUBLIC_SECRET_FLAG, MIN_OVERRIDE_REASON_LENGTH, buildOverrideRecord, collectPublicEnvSecrets, formatFindingsMessage, scanDirectoryForPublicSecrets, scanTarballForPublicSecrets, } from '../lib/public-secret-scan.js';
|
|
36
40
|
import { getActiveTokenSync } from '../lib/token-resolver.js';
|
|
41
|
+
import { BYTES_FAREJADOS, checkDeployTargetContract, farejarConteudoDePacote, mensagemSourceAcimaDoTeto, mensagemSourceNaoEhHandler, pareceArquivoDePacote, WORKSPACE_SOURCE_MAX_BYTES, } from '../lib/deploy-target-contract.js';
|
|
37
42
|
import { safeExit } from '../utils/safe-exit.js';
|
|
43
|
+
import { AVISO_AGENTE_PENDENTE, CHAVES_DO_CARREGADOR, CHAVE_SEGREDO_RE, chaveInjetadaPeloCore, comandoParaRegistrarSegredo, problemaNaChaveReservada, } from '../lib/segredo-produto.js';
|
|
44
|
+
import { avisoEnvDoBundlerForaDeUtf8, CodificacaoInvalida, decodificarUtf8, semBomInicial, } from '../lib/texto-utf8.js';
|
|
38
45
|
/**
|
|
39
46
|
* bug_a5786e03 (2026-05-31): o Core/agente só conhecem os stacks canônicos
|
|
40
47
|
* `node|docker|php-apache|static` (PRODUCT_STACKS). `nextjs` é um alias de
|
|
@@ -86,6 +93,113 @@ export function resolveDeployVersion(manifestVersion, explicitVersion, json) {
|
|
|
86
93
|
}
|
|
87
94
|
return version;
|
|
88
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Valida o motivo do `--allow-public-secret` ANTES de qualquer build/upload.
|
|
98
|
+
* `undefined` = flag não passada.
|
|
99
|
+
*/
|
|
100
|
+
export function validateAllowPublicSecretReason(raw, json) {
|
|
101
|
+
if (raw === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
const reason = String(raw).trim();
|
|
104
|
+
if (reason.length < MIN_OVERRIDE_REASON_LENGTH) {
|
|
105
|
+
deployFail(json, 'invalid_override_reason', `${ALLOW_PUBLIC_SECRET_FLAG} exige um motivo de verdade (mínimo ${MIN_OVERRIDE_REASON_LENGTH} caracteres) — ` +
|
|
106
|
+
'ele fica registrado no deploy. Ex.: --allow-public-secret "rotação da chave em andamento, ticket #123".');
|
|
107
|
+
}
|
|
108
|
+
return reason;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 2.30.0 — gate de segredo no pacote PÚBLICO (decisão do dono 2026-10-03: a
|
|
112
|
+
* credencial nrt_ é SÓ DE SERVIDOR). Roda ANTES do upload. Achou → falha
|
|
113
|
+
* FECHADO (exit 1, `public_secret_in_bundle`), a não ser que haja override com
|
|
114
|
+
* motivo — aí segue com aviso alto e devolve o registro pro corpo do deploy.
|
|
115
|
+
*/
|
|
116
|
+
export async function runPublicSecretGate(source, ctx) {
|
|
117
|
+
const lg = scopedLog(ctx.json);
|
|
118
|
+
// Rodada final 2 do cli#81: `.env*` fora de UTF-8 limpo não para a varredura
|
|
119
|
+
// (ela confere o valor como o bundler o lê), mas não passa calado. Em --json
|
|
120
|
+
// o aviso vai ao stderr: o stdout segue com a linha JSON única.
|
|
121
|
+
const envSecrets = await collectPublicEnvSecrets(ctx.cwd, ctx.envFile, process.env, (arquivo, problema) => {
|
|
122
|
+
const aviso = avisoEnvDoBundlerForaDeUtf8(arquivo, problema);
|
|
123
|
+
if (ctx.json)
|
|
124
|
+
console.error(aviso);
|
|
125
|
+
else
|
|
126
|
+
lg.warn(aviso);
|
|
127
|
+
});
|
|
128
|
+
let result;
|
|
129
|
+
try {
|
|
130
|
+
result =
|
|
131
|
+
source.kind === 'tar'
|
|
132
|
+
? await scanTarballForPublicSecrets(source.path, { stack: ctx.stack, envSecrets })
|
|
133
|
+
: await scanDirectoryForPublicSecrets(source.root, { stack: ctx.stack, envSecrets });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
// Não conseguir LER o pacote é falha fechada também (não dá pra afirmar que está limpo).
|
|
137
|
+
deployFail(ctx.json, 'public_secret_scan_failed', `Não consegui varrer o pacote em busca de segredos: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
if (result.skippedLarge.length > 0) {
|
|
140
|
+
lg.warn(`Varredura de segredo: ${result.skippedLarge.length} arquivo(s) público(s) grande(s) demais não foram varridos ` +
|
|
141
|
+
`(ex.: ${result.skippedLarge[0]}).`);
|
|
142
|
+
}
|
|
143
|
+
if (result.findings.length === 0) {
|
|
144
|
+
lg.dim(` Varredura de segredo no pacote público: ok (${result.scannedFiles} arquivo(s)).`);
|
|
145
|
+
return { scanned: true, scannedFiles: result.scannedFiles };
|
|
146
|
+
}
|
|
147
|
+
if (!ctx.allowReason) {
|
|
148
|
+
deployFail(ctx.json, 'public_secret_in_bundle', formatFindingsMessage(result.findings));
|
|
149
|
+
}
|
|
150
|
+
const override = buildOverrideRecord(ctx.allowReason, result.findings);
|
|
151
|
+
lg.warn(formatFindingsMessage(result.findings).split('\n')[0].replace('Deploy BLOQUEADO.', 'Deploy LIBERADO por override.'));
|
|
152
|
+
lg.warn(`OVERRIDE ${ALLOW_PUBLIC_SECRET_FLAG} — motivo registrado no deploy: "${ctx.allowReason}". ` +
|
|
153
|
+
'Trate a chave exposta como VAZADA: rotacione-a assim que possível.');
|
|
154
|
+
return { scanned: true, scannedFiles: result.scannedFiles, override };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* bug_678ddc98 (elo que faltava): decide se este deploy precisa DIZER ao Core
|
|
158
|
+
* qual é o entrypoint, em vez de deixar o agente adivinhar.
|
|
159
|
+
*
|
|
160
|
+
* O #68 ensinou o `neetru build` a empacotar monorepo e a calcular o entrypoint
|
|
161
|
+
* aninhado (`apps/web/server.js`), mas a cadeia parava no `manifest.json`:
|
|
162
|
+
* `manifest.entrypoint` nunca foi lido por ninguém — era campo write-only.
|
|
163
|
+
* Num monorepo o tarball fica assim:
|
|
164
|
+
*
|
|
165
|
+
* <versionDir>/node_modules/… ← tracejado, na RAIZ
|
|
166
|
+
* <versionDir>/packages/ui/… ← pacotes internos do workspace
|
|
167
|
+
* <versionDir>/apps/web/server.js ← entrypoint REAL
|
|
168
|
+
* <versionDir>/apps/web/package.json ← o package.json DO APP mora aqui
|
|
169
|
+
*
|
|
170
|
+
* O agente (`neetru-agent-service-vm/src/lib/deploy-plan.ts:117-124`) infere o
|
|
171
|
+
* comando de start olhando SÓ a raiz do versionDir: `server.js` na raiz,
|
|
172
|
+
* `.next/` na raiz, `.next/standalone/server.js`, e `scripts.start` do
|
|
173
|
+
* `package.json` da raiz. Em monorepo os três primeiros não existem, e o
|
|
174
|
+
* quarto não ajuda: o Next.js escreve o package.json do app em
|
|
175
|
+
* `<appPrefix>/package.json` (`next/dist/build/utils.js:1197`), então o que
|
|
176
|
+
* houver na raiz é o package.json do WORKSPACE — que ou não tem `start`, ou
|
|
177
|
+
* tem um `start` de ferramenta de monorepo (`turbo start`), nunca
|
|
178
|
+
* `node apps/web/server.js`. Nos dois casos o agente erra: cai no fallback
|
|
179
|
+
* `'npm start'` (deploy-plan.ts:124) e o systemd entra em crash-loop.
|
|
180
|
+
*
|
|
181
|
+
* REGRA: só mandamos o entrypoint quando o agente NÃO consegue acertar sozinho,
|
|
182
|
+
* isto é quando o standalone está aninhado (`appPrefix` preenchido). Para app
|
|
183
|
+
* único o agente já resolve certo — e mandar `pkg.main`/`index.js` ali
|
|
184
|
+
* ATROPELARIA o `npm start` de produtos como o gestovendas, que dependem de
|
|
185
|
+
* `scripts.start`. Conserto que quebra o caso que funcionava não é conserto.
|
|
186
|
+
*
|
|
187
|
+
* Também não mandamos para stack não-node: ali `entrypoint` é `index.html` /
|
|
188
|
+
* `index.php` / referência de imagem, que não é "arquivo para o `node` rodar".
|
|
189
|
+
*
|
|
190
|
+
* ATENÇÃO — a cadeia ainda NÃO fecha só com isto: o Core descarta o campo hoje
|
|
191
|
+
* (`deployRequestSchema` não o declara, e objeto zod sem `.passthrough()` remove
|
|
192
|
+
* chave desconhecida em silêncio). Ver "o que falta no Core" na descrição do PR.
|
|
193
|
+
*/
|
|
194
|
+
export function entrypointParaDeploy(manifest) {
|
|
195
|
+
if (!manifest)
|
|
196
|
+
return undefined;
|
|
197
|
+
if (manifest.stack !== 'node')
|
|
198
|
+
return undefined;
|
|
199
|
+
if (!manifest.appPrefix)
|
|
200
|
+
return undefined;
|
|
201
|
+
return manifest.entrypoint ?? undefined;
|
|
202
|
+
}
|
|
89
203
|
/** `true` pra `prod`/`production` (o Core normaliza `production`→`prod`). */
|
|
90
204
|
export function isProdEnv(envName) {
|
|
91
205
|
const e = (envName ?? '').trim().toLowerCase();
|
|
@@ -94,7 +208,7 @@ export function isProdEnv(envName) {
|
|
|
94
208
|
/**
|
|
95
209
|
* M7 (auditoria cli/v1 2026-09-29): `POST /api/cli/v1/deploy` com
|
|
96
210
|
* `environmentId` prod exige step-up MFA no Core (mesmo gate de `promote`).
|
|
97
|
-
* Resolve os headers (
|
|
211
|
+
* Resolve os headers (--mfa-token → sessão 1h → prompt TOTP; fail-closed em
|
|
98
212
|
* --json/--non-interactive/sem TTY) só quando o alvo é prod. Chamado logo
|
|
99
213
|
* antes do POST — depois do build/upload — pra o TOTP não vencer no meio.
|
|
100
214
|
*/
|
|
@@ -108,26 +222,74 @@ export async function resolveDeployStepUpHeaders(opts, envName, nonInteractive)
|
|
|
108
222
|
});
|
|
109
223
|
return headers;
|
|
110
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Mostra os `warnings[]` que o Core devolve no deploy.
|
|
227
|
+
*
|
|
228
|
+
* Medido em 2026-10-12: o Core devolve `warnings` nos TRES alvos e o comentario
|
|
229
|
+
* dele diz, literalmente, "`warnings[]` na resposta JSON (o CLI exibe pro
|
|
230
|
+
* operador)" (`cli/v1/deploy/route.ts:309`). `grep warnings` em `deploy.ts`
|
|
231
|
+
* devolvia ZERO — a promessa nunca foi cumprida.
|
|
232
|
+
*
|
|
233
|
+
* O caso que mais doia: a CLI manda o ambiente default e o Core normaliza, e o
|
|
234
|
+
* aviso de que `environmentId` foi ASSUMIDO disparava em todo deploy sem
|
|
235
|
+
* `--env` — e sumia 100% das vezes. O operador achava que tinha escolhido.
|
|
236
|
+
*
|
|
237
|
+
* Vai pro STDERR de proposito: em `--json` o stdout e contrato de maquina (uma
|
|
238
|
+
* linha JSON) e nao pode ser poluido. Quem le JSON ja recebe `warnings` no
|
|
239
|
+
* proprio corpo.
|
|
240
|
+
*/
|
|
241
|
+
export function mostrarAvisosDoCore(warnings, json) {
|
|
242
|
+
if (!Array.isArray(warnings))
|
|
243
|
+
return;
|
|
244
|
+
const linhas = warnings.filter((w) => typeof w === 'string' && w.trim() !== '');
|
|
245
|
+
if (linhas.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
if (json) {
|
|
248
|
+
for (const w of linhas)
|
|
249
|
+
process.stderr.write(`[aviso do Core] ${w}\n`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const w of linhas)
|
|
253
|
+
log.warn(` aviso do Core: ${w}`);
|
|
254
|
+
}
|
|
111
255
|
const TERMINAL_STATUSES = new Set(['success', 'failed', 'cancelled']);
|
|
112
|
-
/**
|
|
113
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Estágios de ambiente válidos para o deploy workspace/cloud-run. P105: era
|
|
258
|
+
* `new Set(['dev','staging','prod'])` escrito à mão; agora sai da tabela
|
|
259
|
+
* `VOCABULARIOS_DE_AMBIENTE` (`lib/envs.ts`), derivada de
|
|
260
|
+
* `TENANT_ENVIRONMENT_IDS`.
|
|
261
|
+
*/
|
|
262
|
+
const WORKSPACE_ENVS = new Set(DEPLOY_ENVIRONMENT_IDS);
|
|
263
|
+
/**
|
|
264
|
+
* `--env` do deploy workspace/cloud-run: fora do vocabulário, encerra com exit
|
|
265
|
+
* 1 e os valores válidos da MESMA lista que validou (`invalid_env`, com `flag`,
|
|
266
|
+
* `value` e `valid` no JSON). Exportada para teste.
|
|
267
|
+
*/
|
|
268
|
+
export function validarAmbienteDoDeploy(envName, json) {
|
|
269
|
+
if (WORKSPACE_ENVS.has(envName))
|
|
270
|
+
return;
|
|
271
|
+
exitWithCliError(invalidChoiceError({
|
|
272
|
+
flag: '--env',
|
|
273
|
+
value: envName,
|
|
274
|
+
valid: DEPLOY_ENVIRONMENT_IDS,
|
|
275
|
+
subject: 'Ambiente',
|
|
276
|
+
code: 'invalid_env',
|
|
277
|
+
}), json);
|
|
278
|
+
}
|
|
114
279
|
/**
|
|
115
280
|
* Mapeia o `envName` do deploy ('dev' | 'staging' | 'prod' — vocabulário do
|
|
116
281
|
* `--env` deste comando) pro `targetEnv` que `neetru build` entende
|
|
117
282
|
* ('dev' | 'workspace' | 'prod' — vocabulário de `BuildOptions`/SDK).
|
|
118
283
|
* 'staging' vira 'workspace' (mesmo conceito de ambiente não-dev/não-prod).
|
|
119
284
|
* Valor desconhecido cai em 'prod' (mesmo default conservador de `runBuild`).
|
|
285
|
+
* P105: a correspondência sai de `VOCABULARIOS_DE_AMBIENTE`, não de `if`s.
|
|
120
286
|
*
|
|
121
287
|
* Fix (2026-07-22, achado da revisão adversarial): sem isso, todo build
|
|
122
288
|
* implícito de `neetru deploy --env dev`/`--env staging` (quando não há
|
|
123
289
|
* artifact em cache) forçava NEXT_PUBLIC_NEETRU_ENV=prod incondicionalmente.
|
|
124
290
|
*/
|
|
125
291
|
export function deployEnvToBuildTargetEnv(envName) {
|
|
126
|
-
|
|
127
|
-
return 'dev';
|
|
128
|
-
if (envName === 'staging')
|
|
129
|
-
return 'workspace';
|
|
130
|
-
return 'prod';
|
|
292
|
+
return deployEnvParaRuntime(envName) ?? 'prod';
|
|
131
293
|
}
|
|
132
294
|
/** Candidatos default de arquivo de source para um deploy workspace. */
|
|
133
295
|
const WORKSPACE_SOURCE_CANDIDATES = [
|
|
@@ -168,6 +330,16 @@ async function loadLocalConfig(cwd) {
|
|
|
168
330
|
}
|
|
169
331
|
return null;
|
|
170
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* `runtime` do neetru.config.json do diretório atual, só quando o config é
|
|
335
|
+
* DESTE produto. `--product outro` com o config do diretório atual: o `runtime`
|
|
336
|
+
* lido é de OUTRO produto e não diz nada sobre este deploy (revisão do PR #74).
|
|
337
|
+
* Sem `--product`, ou config sem `slug`, conta como deste produto.
|
|
338
|
+
*/
|
|
339
|
+
function runtimeDoConfigDesteProduto(cfg, product) {
|
|
340
|
+
const configEhDesteProduto = !product || !cfg?.slug || cfg.slug === product;
|
|
341
|
+
return configEhDesteProduto ? cfg?.runtime : undefined;
|
|
342
|
+
}
|
|
171
343
|
/**
|
|
172
344
|
* Nomes de variáveis que NUNCA devem ser injetados no deploy —
|
|
173
345
|
* podem alterar o comportamento do runtime de formas perigosas.
|
|
@@ -241,10 +413,82 @@ const SDK_CRED_ENV_KEYS = new Set([
|
|
|
241
413
|
'NEETRU_API_KEY',
|
|
242
414
|
'NEETRU_CLIENT_ID',
|
|
243
415
|
]);
|
|
244
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Aviso do segredo descartado no `--env-file` de um deploy VM.
|
|
418
|
+
*
|
|
419
|
+
* bug_8b85353b (Parte 3): até aqui a mensagem mandava "usar o Secret Manager e
|
|
420
|
+
* referenciar por nome na VM" — mecanismo que NÃO existia para segredo do
|
|
421
|
+
* próprio produto (era a 4ª cópia do padrão que o core#632 corrigiu nas outras
|
|
422
|
+
* três). O caminho real é `neetru secrets set`, que grava no registro
|
|
423
|
+
* `product_secrets` do Core com o valor direto no Secret Manager. `env` é o
|
|
424
|
+
* `--env` do deploy, para o comando sugerido já sair com o ambiente certo.
|
|
425
|
+
*
|
|
426
|
+
* Revisão do #81 (M1): o comando só é sugerido quando o Core o aceitaria.
|
|
427
|
+
* Chave reservada (o Core já a entrega ao app: credencial SDK/OIDC/CLI,
|
|
428
|
+
* módulo, unit) é recusada em `assertValidProductSecretKey`, e nome fora de
|
|
429
|
+
* CAIXA_ALTA é recusado pela própria CLI (`problemaNaChave`) — sugerir
|
|
430
|
+
* `neetru secrets set` nesses casos mandava o dev para um caminho que falha.
|
|
431
|
+
*/
|
|
432
|
+
export function avisoSegredoDescartadoNaVm(chave, env) {
|
|
433
|
+
const inicio = `Variável com aparência de segredo ignorada (${chave}): `;
|
|
434
|
+
if (CHAVES_DO_CARREGADOR.includes(chave)) {
|
|
435
|
+
return inicio + 'ela muda como o processo carrega código e não pode vir de um segredo do produto.';
|
|
436
|
+
}
|
|
437
|
+
if (chaveInjetadaPeloCore(chave)) {
|
|
438
|
+
return (inicio +
|
|
439
|
+
'o Core já a entrega ao app na VM por outro canal (credencial SDK, OIDC ou CLI, módulo ' +
|
|
440
|
+
'ativo, ambiente ou unit do app). Não precisa passá-la no --env-file nem registrá-la como ' +
|
|
441
|
+
'segredo do produto.');
|
|
442
|
+
}
|
|
443
|
+
if (!CHAVE_SEGREDO_RE.test(chave)) {
|
|
444
|
+
return (inicio +
|
|
445
|
+
'segredo não vai no deploy por variável de ambiente (ficaria gravado no Firestore, que ' +
|
|
446
|
+
'toda a equipe lê), e o registro de segredos do produto só aceita nome em CAIXA_ALTA ' +
|
|
447
|
+
'(letras, dígitos e underscore, começando por letra, de 2 a 64 caracteres). Renomeie a ' +
|
|
448
|
+
`variável no app e registre-a com \`${comandoParaRegistrarSegredo('<NOME>', env)}\`.`);
|
|
449
|
+
}
|
|
450
|
+
return (inicio +
|
|
451
|
+
'segredo não vai no deploy por variável de ambiente (ficaria gravado no Firestore, que toda ' +
|
|
452
|
+
`a equipe lê). Registre-o com \`${comandoParaRegistrarSegredo(chave, env)}\` — o valor vai ` +
|
|
453
|
+
'direto para o Secret Manager; use --generate quando o Core puder sortear o valor.');
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* O aviso desta chave manda para `neetru secrets set`? Não manda quando o
|
|
457
|
+
* Core reserva a chave — aí o aviso de que o agente ainda não entrega
|
|
458
|
+
* segredos do produto também não cabe.
|
|
459
|
+
*/
|
|
460
|
+
export function segredoDescartadoApontaSecretsSet(chave) {
|
|
461
|
+
return problemaNaChaveReservada(chave) === null;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Lê o `--env-file` do deploy e devolve o texto. Revisão do cli#81 (N-r3-1):
|
|
465
|
+
* `readFile(…, 'utf8')` trocava cada byte que não é UTF-8 por U+FFFD sem aviso
|
|
466
|
+
* — um .env ANSI com `NOME_LOJA=Café` ia ao deploy como `Caf\uFFFD`. Agora o
|
|
467
|
+
* arquivo é lido em bytes e recusado com `invalid_encoding`, pela mesma regra
|
|
468
|
+
* do `neetru secrets set --from-file`. `runDeploy` chama esta função antes do
|
|
469
|
+
* catálogo, de qualquer pergunta e de qualquer chamada de rede (rodada final 3).
|
|
470
|
+
*/
|
|
471
|
+
async function lerEnvFileDoDeploy(envFile, json) {
|
|
472
|
+
const bruto = await fsp.readFile(envFile);
|
|
473
|
+
try {
|
|
474
|
+
return semBomInicial(decodificarUtf8(bruto, 'env-file'));
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
if (err instanceof CodificacaoInvalida)
|
|
478
|
+
deployFail(json, 'invalid_encoding', err.message);
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Monta as variáveis do `--env-file` que `lerEnvFileDoDeploy` já leu. Fica
|
|
484
|
+
* para depois do `pickTarget` porque os avisos dependem do alvo.
|
|
485
|
+
*/
|
|
486
|
+
function envVarsDoEnvFile(content, target = 'cloud-run', json,
|
|
487
|
+
/** `--env` do deploy (só para o aviso de segredo descartado na VM). */
|
|
488
|
+
env) {
|
|
245
489
|
const lg = scopedLog(json);
|
|
246
|
-
const content = await fsp.readFile(envFile, 'utf8');
|
|
247
490
|
const out = {};
|
|
491
|
+
let descartouSegredoNaVm = false;
|
|
248
492
|
if (target === 'vm') {
|
|
249
493
|
// (3) Lembrete único: a credencial SDK não precisa ir no --env-file.
|
|
250
494
|
lg.info('A credencial SDK é injetada automaticamente na VM pelo Core/agente — ' +
|
|
@@ -290,13 +534,23 @@ async function loadEnvFile(envFile, target = 'cloud-run', json) {
|
|
|
290
534
|
}
|
|
291
535
|
// (1) Segredos são dropados aqui (o Core rejeitaria com HTTP 400).
|
|
292
536
|
if (isSecretEnvVarKey(k)) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
537
|
+
if (target === 'vm') {
|
|
538
|
+
if (segredoDescartadoApontaSecretsSet(k))
|
|
539
|
+
descartouSegredoNaVm = true;
|
|
540
|
+
lg.warn(avisoSegredoDescartadoNaVm(k, env));
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
lg.warn(`Variável com aparência de segredo ignorada (${k}): segredos não podem ` +
|
|
544
|
+
'ir no deploy via env (vazariam no Firestore, legível por todo o staff). ' +
|
|
545
|
+
'Use o Secret Manager e referencie por nome na VM.');
|
|
546
|
+
}
|
|
296
547
|
continue;
|
|
297
548
|
}
|
|
298
549
|
out[k] = v;
|
|
299
550
|
}
|
|
551
|
+
// Uma vez só, depois da lista: o registro ainda não chega ao app na VM.
|
|
552
|
+
if (descartouSegredoNaVm)
|
|
553
|
+
lg.warn(AVISO_AGENTE_PENDENTE);
|
|
300
554
|
return out;
|
|
301
555
|
}
|
|
302
556
|
// handleApiError unificado em ../lib/api-error.js (auditoria qualidade 2026-06-20).
|
|
@@ -335,7 +589,7 @@ async function pickProduct(opts, nonInteractive, cwd) {
|
|
|
335
589
|
]);
|
|
336
590
|
return slug;
|
|
337
591
|
}
|
|
338
|
-
async function pickTarget(opts, nonInteractive) {
|
|
592
|
+
export async function pickTarget(opts, nonInteractive, runtime) {
|
|
339
593
|
if (opts.target)
|
|
340
594
|
return opts.target;
|
|
341
595
|
// Não-interativo (CI/scripts) mantém `vm` como default — mudar pra workspace
|
|
@@ -343,17 +597,27 @@ async function pickTarget(opts, nonInteractive) {
|
|
|
343
597
|
// interativo o menu já oferece workspace como recomendado/primeiro.
|
|
344
598
|
if (nonInteractive)
|
|
345
599
|
return 'vm';
|
|
600
|
+
// Produto `runtime: nextjs` (o padrão do `neetru init`): o workspace roda UM
|
|
601
|
+
// handler .ts, não um app Next.js. Recomendar "Workspace" aqui levava quem
|
|
602
|
+
// aceita o padrão direto ao aviso do contrato (confirmação final do PR #74).
|
|
603
|
+
const ehNext = runtime === 'nextjs';
|
|
346
604
|
const { target } = await inquirer.prompt([
|
|
347
605
|
{
|
|
348
606
|
type: 'list',
|
|
349
607
|
name: 'target',
|
|
350
608
|
message: 'Onde deployar?',
|
|
351
|
-
default: 'workspace',
|
|
352
|
-
choices:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
609
|
+
default: ehNext ? 'vm' : 'workspace',
|
|
610
|
+
choices: ehNext
|
|
611
|
+
? [
|
|
612
|
+
{ name: 'VM existente — serve o app Next.js (recomendado)', value: 'vm' },
|
|
613
|
+
{ name: 'Cloud Run — serviço dedicado, escala a zero (imagem)', value: 'cloud-run' },
|
|
614
|
+
{ name: 'Workspace — só UM handler .ts, não o app Next.js', value: 'workspace' },
|
|
615
|
+
]
|
|
616
|
+
: [
|
|
617
|
+
{ name: 'Workspace — runtime compartilhado, mais barato (recomendado)', value: 'workspace' },
|
|
618
|
+
{ name: 'Cloud Run — serviço dedicado, escala a zero', value: 'cloud-run' },
|
|
619
|
+
{ name: 'VM existente — custo fixo, sem cold start', value: 'vm' },
|
|
620
|
+
],
|
|
357
621
|
},
|
|
358
622
|
]);
|
|
359
623
|
return target;
|
|
@@ -566,7 +830,15 @@ async function uploadArtifactToGcs(tarballAbs, productSlug, envName, sizeBytes,
|
|
|
566
830
|
async function resolveArtifact(opts, productSlug, envName, cwd) {
|
|
567
831
|
// Caso 1: caller já fez upload e passou URL+sha pronta.
|
|
568
832
|
if (opts.artifactUrl && opts.artifactSha256) {
|
|
569
|
-
|
|
833
|
+
// 2.30.0: artefato remoto não passa pelo CLI — não dá pra varrer. Avisa (o
|
|
834
|
+
// gate vale pro fluxo padrão build→upload e pro --artifact local).
|
|
835
|
+
scopedLog(opts.json).warn('Varredura de segredo NÃO feita: --artifact-url aponta um pacote remoto. ' +
|
|
836
|
+
'Prefira `neetru deploy` com build local ou --artifact <arquivo> (que são varridos).');
|
|
837
|
+
return {
|
|
838
|
+
artifactUrl: opts.artifactUrl,
|
|
839
|
+
artifactSha256: opts.artifactSha256,
|
|
840
|
+
publicSecretScan: { scanned: false, reason: 'remote_artifact' },
|
|
841
|
+
};
|
|
570
842
|
}
|
|
571
843
|
// Decide se vamos fazer upload pra GCS ou manter file://.
|
|
572
844
|
const keepLocal = shouldKeepLocalArtifact(opts);
|
|
@@ -584,12 +856,15 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
|
|
|
584
856
|
hash.update(chunk);
|
|
585
857
|
const stat = await fsp.stat(abs);
|
|
586
858
|
const sha256 = hash.digest('hex');
|
|
859
|
+
// 2.30.0: varredura de segredo ANTES de qualquer envio.
|
|
860
|
+
const publicSecretScan = await runPublicSecretGate({ kind: 'tar', path: abs }, { cwd, envFile: opts.envFile, stack: opts.stack, allowReason: opts.allowPublicSecret, json: opts.json });
|
|
587
861
|
if (keepLocal) {
|
|
588
862
|
lg.warn('Modo --local-artifact: mandando file:// pro Core. Só funciona se o agent compartilha FS com este CLI.');
|
|
589
863
|
return {
|
|
590
864
|
artifactUrl: `file://${abs.replace(/\\/g, '/')}`,
|
|
591
865
|
artifactSha256: sha256,
|
|
592
866
|
artifactSizeBytes: stat.size,
|
|
867
|
+
publicSecretScan,
|
|
593
868
|
};
|
|
594
869
|
}
|
|
595
870
|
const uploaded = await uploadArtifactToGcs(abs, productSlug, envName, stat.size, opts.json);
|
|
@@ -598,6 +873,7 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
|
|
|
598
873
|
artifactSha256: sha256,
|
|
599
874
|
artifactSizeBytes: stat.size,
|
|
600
875
|
deployId: uploaded.deployId,
|
|
876
|
+
publicSecretScan,
|
|
601
877
|
};
|
|
602
878
|
}
|
|
603
879
|
// Caso 3: procura manifest existente, senão chama runBuild.
|
|
@@ -638,6 +914,14 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
|
|
|
638
914
|
});
|
|
639
915
|
}
|
|
640
916
|
const tarAbs = path.resolve(cwd, manifest.tarball);
|
|
917
|
+
// 2.30.0: varredura de segredo no pacote que VAI ser enviado, antes do upload.
|
|
918
|
+
const publicSecretScan = await runPublicSecretGate({ kind: 'tar', path: tarAbs }, {
|
|
919
|
+
cwd,
|
|
920
|
+
envFile: opts.envFile,
|
|
921
|
+
stack: manifest.stack ?? opts.stack,
|
|
922
|
+
allowReason: opts.allowPublicSecret,
|
|
923
|
+
json: opts.json,
|
|
924
|
+
});
|
|
641
925
|
if (keepLocal) {
|
|
642
926
|
lg.warn('Modo --local-artifact: mandando file:// pro Core. Só funciona se o agent compartilha FS com este CLI.');
|
|
643
927
|
return {
|
|
@@ -645,6 +929,7 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
|
|
|
645
929
|
artifactSha256: manifest.sha256,
|
|
646
930
|
artifactSizeBytes: manifest.sizeBytes,
|
|
647
931
|
manifest,
|
|
932
|
+
publicSecretScan,
|
|
648
933
|
};
|
|
649
934
|
}
|
|
650
935
|
const uploaded = await uploadArtifactToGcs(tarAbs, productSlug, envName, manifest.sizeBytes, opts.json);
|
|
@@ -654,6 +939,7 @@ async function resolveArtifact(opts, productSlug, envName, cwd) {
|
|
|
654
939
|
artifactSizeBytes: manifest.sizeBytes,
|
|
655
940
|
manifest,
|
|
656
941
|
deployId: uploaded.deployId,
|
|
942
|
+
publicSecretScan,
|
|
657
943
|
};
|
|
658
944
|
}
|
|
659
945
|
async function pollDeploymentStatus(deploymentId, json) {
|
|
@@ -888,16 +1174,32 @@ async function probeDeployedHealth(domain) {
|
|
|
888
1174
|
* Resolve o arquivo de source `.ts` de um deploy workspace. Ordem de busca:
|
|
889
1175
|
* `--artifact <arquivo>` → campo `source` do neetru.config.json → candidatos
|
|
890
1176
|
* default (`handler.ts`, `src/handler.ts`, `index.ts`, `src/index.ts`).
|
|
1177
|
+
*
|
|
1178
|
+
* Devolve também os primeiros BYTES CRUS (`inicio`) e de onde o arquivo veio
|
|
1179
|
+
* (`origem`): o farejador de pacote precisa dos bytes antes da decodificação
|
|
1180
|
+
* UTF-8 (depois dela o gzip já virou U+FFFD) e a mensagem de recusa precisa
|
|
1181
|
+
* dizer qual porta apontou o arquivo.
|
|
891
1182
|
*/
|
|
892
1183
|
async function resolveWorkspaceSource(opts, cwd) {
|
|
893
|
-
const cfg =
|
|
894
|
-
const candidates = [
|
|
895
|
-
|
|
896
|
-
|
|
1184
|
+
const cfg = await loadLocalConfig(cwd);
|
|
1185
|
+
const candidates = [
|
|
1186
|
+
{ caminho: opts.artifact, origem: 'artifact' },
|
|
1187
|
+
{ caminho: cfg?.source, origem: 'config' },
|
|
1188
|
+
...WORKSPACE_SOURCE_CANDIDATES.map((c) => ({ caminho: c, origem: 'padrao' })),
|
|
1189
|
+
];
|
|
1190
|
+
for (const { caminho, origem } of candidates) {
|
|
1191
|
+
if (typeof caminho !== 'string' || caminho.length === 0)
|
|
1192
|
+
continue;
|
|
1193
|
+
const abs = path.resolve(cwd, caminho);
|
|
897
1194
|
if (fileExists(abs)) {
|
|
1195
|
+
// Uma leitura só: `toString('utf8')` é exatamente o que
|
|
1196
|
+
// `readFile(abs, 'utf8')` fazia.
|
|
1197
|
+
const bruto = await fsp.readFile(abs);
|
|
898
1198
|
return {
|
|
899
|
-
relPath: path.relative(cwd, abs) ||
|
|
900
|
-
content:
|
|
1199
|
+
relPath: path.relative(cwd, abs) || caminho,
|
|
1200
|
+
content: bruto.toString('utf8'),
|
|
1201
|
+
inicio: bruto.subarray(0, BYTES_FAREJADOS),
|
|
1202
|
+
origem,
|
|
901
1203
|
};
|
|
902
1204
|
}
|
|
903
1205
|
}
|
|
@@ -921,11 +1223,38 @@ const WORKSPACE_SOURCE_GCS_THRESHOLD_BYTES = 1.5 * 1024 * 1024;
|
|
|
921
1223
|
* inline. O Core aceita os dois (campo `sourceUrl` no schema).
|
|
922
1224
|
*/
|
|
923
1225
|
async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractive) {
|
|
924
|
-
|
|
925
|
-
deployFail(opts.json, 'invalid_env', `Ambiente inválido: "${envName}". Use dev, staging ou prod.`);
|
|
926
|
-
}
|
|
1226
|
+
validarAmbienteDoDeploy(envName, opts.json);
|
|
927
1227
|
const source = await resolveWorkspaceSource(opts, cwd);
|
|
928
1228
|
const sourceSizeBytes = Buffer.byteLength(source.content, 'utf8');
|
|
1229
|
+
// bug_a8375858 — PACOTE no workspace, pela porta que o gate não vê.
|
|
1230
|
+
//
|
|
1231
|
+
// O gate de contrato (`deploy-target-contract.ts`) só enxerga a extensão de
|
|
1232
|
+
// `--artifact`. Um pacote apontado pelo `source` do neetru.config.json, ou
|
|
1233
|
+
// com extensão que a lista não prevê (`.bin`, nome sem extensão…), chegava
|
|
1234
|
+
// até aqui e só batia no teto abaixo. Lido como UTF-8 o binário incha ~1,75x,
|
|
1235
|
+
// então um pacote abaixo de ~1,15 MiB passava no teto, subia e voltava do
|
|
1236
|
+
// Core como erro de compilação do esbuild, que não diz a causa (revisão do
|
|
1237
|
+
// PR #74). Recusa aqui, ANTES de qualquer signed URL ou POST, pelo nome e
|
|
1238
|
+
// pelos bytes crus — e vem ANTES do teto, pra que um pacote grande ouça
|
|
1239
|
+
// "é pacote" e não "é grande".
|
|
1240
|
+
const motivoDePacote = pareceArquivoDePacote(source.relPath)
|
|
1241
|
+
? 'a extensão é de pacote de aplicação'
|
|
1242
|
+
: farejarConteudoDePacote(source.inicio);
|
|
1243
|
+
if (motivoDePacote) {
|
|
1244
|
+
deployFail(opts.json, 'artifact_nao_eh_handler_workspace', mensagemSourceNaoEhHandler(source.relPath, motivoDePacote, source.origem));
|
|
1245
|
+
}
|
|
1246
|
+
// bug_a8375858 — teto de source, depois da leitura: na prática, o handler
|
|
1247
|
+
// que simplesmente cresceu demais.
|
|
1248
|
+
//
|
|
1249
|
+
// O que faltava: a CLI conhecia só o limiar de 1.5 MiB do bug_eae4bef6, que
|
|
1250
|
+
// decide *inline vs GCS*, e NUNCA o teto absoluto do Core
|
|
1251
|
+
// (`WORKSPACE_SOURCE_MAX_BYTES`, 2 MiB, aplicado nos dois caminhos). Então
|
|
1252
|
+
// tudo acima de 2 MiB subia pro GCS pra só depois voltar 413 "Falha ao
|
|
1253
|
+
// resolver a source do deploy" — mensagem que aponta a dimensão errada.
|
|
1254
|
+
if (sourceSizeBytes > WORKSPACE_SOURCE_MAX_BYTES) {
|
|
1255
|
+
const cfgTeto = await loadLocalConfig(cwd);
|
|
1256
|
+
deployFail(opts.json, 'source_acima_do_teto_workspace', mensagemSourceAcimaDoTeto(source.relPath, sourceSizeBytes, cfgTeto?.runtime));
|
|
1257
|
+
}
|
|
929
1258
|
if (!nonInteractive) {
|
|
930
1259
|
log.heading('Resumo');
|
|
931
1260
|
log.dim(` produto: ${productSlug}`);
|
|
@@ -1026,6 +1355,7 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
|
|
|
1026
1355
|
headers: mfaHeaders,
|
|
1027
1356
|
body: deployBody,
|
|
1028
1357
|
});
|
|
1358
|
+
mostrarAvisosDoCore(resp.warnings, opts.json);
|
|
1029
1359
|
}
|
|
1030
1360
|
catch (err) {
|
|
1031
1361
|
spinner.fail('Falha ao publicar.');
|
|
@@ -1033,9 +1363,11 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
|
|
|
1033
1363
|
}
|
|
1034
1364
|
if (resp.status === 'live') {
|
|
1035
1365
|
spinner.succeed(`Deploy ${chalk.bold('live')} · ${resp.deploymentId}`);
|
|
1366
|
+
const docsSync = await postDeployDocs(opts, cwd, envName);
|
|
1036
1367
|
if (opts.json) {
|
|
1037
1368
|
console.log(JSON.stringify({
|
|
1038
1369
|
ok: true,
|
|
1370
|
+
...(docsSync ? { docs: { sync: docsSync } } : {}),
|
|
1039
1371
|
status: 'live',
|
|
1040
1372
|
target: 'workspace',
|
|
1041
1373
|
deploymentId: resp.deploymentId,
|
|
@@ -1053,9 +1385,11 @@ async function runWorkspaceDeploy(opts, productSlug, envName, cwd, nonInteractiv
|
|
|
1053
1385
|
// aplicou o deploy mas ainda não confirmou saúde. Sucesso COM AVISO — não
|
|
1054
1386
|
// tratamos como falha nem fingimos "Live" confirmado.
|
|
1055
1387
|
spinner.warn(`Deploy aplicado — ${deployStatusLabel(resp.status)} · ${resp.deploymentId}`);
|
|
1388
|
+
const docsSync = await postDeployDocs(opts, cwd, envName);
|
|
1056
1389
|
if (opts.json) {
|
|
1057
1390
|
console.log(JSON.stringify({
|
|
1058
1391
|
ok: true,
|
|
1392
|
+
...(docsSync ? { docs: { sync: docsSync } } : {}),
|
|
1059
1393
|
status: resp.status,
|
|
1060
1394
|
verified: false,
|
|
1061
1395
|
target: 'workspace',
|
|
@@ -1358,28 +1692,20 @@ async function resolveCloudRunImage(opts, productSlug, envName, cwd) {
|
|
|
1358
1692
|
* Deploy `target=cloud-run` — cria/atualiza o serviço Cloud Run dedicado do
|
|
1359
1693
|
* ambiente. Síncrono: o POST retorna com o desfecho (`live`/`failed`/...).
|
|
1360
1694
|
*/
|
|
1361
|
-
async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
' Exemplo: --image=us-central1-docker.pkg.dev/neetru/gestovendas-staging/app:1.0.0\n' +
|
|
1376
|
-
'\n' +
|
|
1377
|
-
' 2. --registry <host|host/project/repo> — CLI usa Cloud Build remoto automaticamente.\n' +
|
|
1378
|
-
' Não exige Docker instalado. Requer gcloud autenticado.\n' +
|
|
1379
|
-
' Exemplo: --registry=us-central1-docker.pkg.dev/neetru/gestovendas-staging\n' +
|
|
1380
|
-
'\n' +
|
|
1381
|
-
' 3. Mude o target pra vm ou workspace se quiser usar o tarball:\n' +
|
|
1382
|
-
' --target=vm --artifact=' + (opts.artifact ?? opts.artifactUrl ?? '<file>'));
|
|
1695
|
+
async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive,
|
|
1696
|
+
/** O `--env-file` já lido e conferido por `runDeploy` (`lerEnvFileDoDeploy`). */
|
|
1697
|
+
envFileTexto) {
|
|
1698
|
+
validarAmbienteDoDeploy(envName, opts.json);
|
|
1699
|
+
// bug_ec17af08 (cloud-run não aceita tarball) agora mora na tabela do
|
|
1700
|
+
// `deploy-target-contract.ts`, avaliada em `runDeploy` logo após o
|
|
1701
|
+
// `pickTarget` — ANTES de qualquer build/upload, pra TODOS os alvos. O
|
|
1702
|
+
// código de erro e o texto são os mesmos; só deixou de ser um `if` solto.
|
|
1703
|
+
// Rodadas finais 2 e 3 do cli#81: o `--env-file` era lido depois de
|
|
1704
|
+
// `resolveCloudRunImage`, que manda a imagem ao Cloud Build. Agora
|
|
1705
|
+
// `runDeploy` o lê e confere antes de tudo; aqui só se montam as variáveis.
|
|
1706
|
+
let envVars;
|
|
1707
|
+
if (envFileTexto !== undefined) {
|
|
1708
|
+
envVars = envVarsDoEnvFile(envFileTexto, 'cloud-run', opts.json);
|
|
1383
1709
|
}
|
|
1384
1710
|
// bug_863ae5: pre-flight IAM check — abort cedo se SA Core não tiver permissão.
|
|
1385
1711
|
// MASTER-HIGH-048 (2.11.0): prefere auth.json sobre Conf legado.
|
|
@@ -1387,11 +1713,17 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
|
|
|
1387
1713
|
if (deployToken) {
|
|
1388
1714
|
await assertIamReady(deployToken, 'deploy.cloud-run', opts.json);
|
|
1389
1715
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1716
|
+
// 2.30.0: varredura de segredo nas pastas públicas ANTES de buildar a imagem.
|
|
1717
|
+
// Com --image (imagem pronta) não há o que varrer aqui — avisa.
|
|
1718
|
+
let publicSecretScan;
|
|
1719
|
+
if (opts.image) {
|
|
1720
|
+
scopedLog(opts.json).warn('Varredura de segredo NÃO feita: --image aponta uma imagem já buildada (o CLI não vê o conteúdo).');
|
|
1721
|
+
publicSecretScan = { scanned: false, reason: 'prebuilt_image' };
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
publicSecretScan = await runPublicSecretGate({ kind: 'dir', root: cwd }, { cwd, envFile: opts.envFile, stack: opts.stack, allowReason: opts.allowPublicSecret, json: opts.json });
|
|
1394
1725
|
}
|
|
1726
|
+
const imageUrl = await resolveCloudRunImage(opts, productSlug, envName, cwd);
|
|
1395
1727
|
if (!nonInteractive) {
|
|
1396
1728
|
log.heading('Resumo');
|
|
1397
1729
|
log.dim(` produto: ${productSlug}`);
|
|
@@ -1421,8 +1753,10 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
|
|
|
1421
1753
|
imageUrl,
|
|
1422
1754
|
...(opts.port ? { port: opts.port } : {}),
|
|
1423
1755
|
...(envVars && Object.keys(envVars).length > 0 ? { envVars } : {}),
|
|
1756
|
+
...(publicSecretScan.override ? { publicSecretOverride: publicSecretScan.override } : {}),
|
|
1424
1757
|
},
|
|
1425
1758
|
});
|
|
1759
|
+
mostrarAvisosDoCore(resp.warnings, opts.json);
|
|
1426
1760
|
}
|
|
1427
1761
|
catch (err) {
|
|
1428
1762
|
spinner.fail('Falha ao publicar.');
|
|
@@ -1430,9 +1764,11 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
|
|
|
1430
1764
|
}
|
|
1431
1765
|
if (resp.status === 'live') {
|
|
1432
1766
|
spinner.succeed(`Deploy ${chalk.bold('live')} · ${resp.deploymentId}`);
|
|
1767
|
+
const docsSync = await postDeployDocs(opts, cwd, envName);
|
|
1433
1768
|
if (opts.json) {
|
|
1434
1769
|
console.log(JSON.stringify({
|
|
1435
1770
|
ok: true,
|
|
1771
|
+
...(docsSync ? { docs: { sync: docsSync } } : {}),
|
|
1436
1772
|
status: 'live',
|
|
1437
1773
|
target: 'cloud-run',
|
|
1438
1774
|
deploymentId: resp.deploymentId,
|
|
@@ -1454,9 +1790,11 @@ async function runCloudRunDeploy(opts, productSlug, envName, cwd, nonInteractive
|
|
|
1454
1790
|
// aplicou o deploy mas ainda não confirmou saúde. Sucesso COM AVISO — não
|
|
1455
1791
|
// tratamos como falha nem fingimos "Live" confirmado.
|
|
1456
1792
|
spinner.warn(`Deploy aplicado — ${deployStatusLabel(resp.status)} · ${resp.deploymentId}`);
|
|
1793
|
+
const docsSync = await postDeployDocs(opts, cwd, envName);
|
|
1457
1794
|
if (opts.json) {
|
|
1458
1795
|
console.log(JSON.stringify({
|
|
1459
1796
|
ok: true,
|
|
1797
|
+
...(docsSync ? { docs: { sync: docsSync } } : {}),
|
|
1460
1798
|
status: resp.status,
|
|
1461
1799
|
verified: false,
|
|
1462
1800
|
target: 'cloud-run',
|
|
@@ -1562,18 +1900,115 @@ async function checkSchemaGate(cwd, opts) {
|
|
|
1562
1900
|
}
|
|
1563
1901
|
return true;
|
|
1564
1902
|
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Protocolo de docs v1 (decisão do dono 2026-10-03): depois de um deploy bem
|
|
1905
|
+
* sucedido, publica os docs no Core SÓ se for produção e `docs.publishOnDeploy`
|
|
1906
|
+
* estiver ligado. Nunca lança — falha do sync não derruba o deploy.
|
|
1907
|
+
*/
|
|
1908
|
+
async function postDeployDocs(opts, cwd, envName) {
|
|
1909
|
+
if (opts.semDocs)
|
|
1910
|
+
return undefined;
|
|
1911
|
+
try {
|
|
1912
|
+
const { deployDocsPostSync } = await import('./docs-produto.js');
|
|
1913
|
+
const r = await deployDocsPostSync(cwd, envName, opts.json);
|
|
1914
|
+
return r.attempted ? r : undefined;
|
|
1915
|
+
}
|
|
1916
|
+
catch {
|
|
1917
|
+
return undefined;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1565
1920
|
export async function runDeploy(opts) {
|
|
1566
1921
|
if (!opts.json)
|
|
1567
1922
|
log.banner();
|
|
1568
1923
|
const cwd = process.cwd();
|
|
1569
1924
|
const nonInteractive = ensureNonInteractive(opts);
|
|
1925
|
+
// 2.30.0: motivo do override validado ANTES de build/upload.
|
|
1926
|
+
opts = { ...opts, allowPublicSecret: validateAllowPublicSecretReason(opts.allowPublicSecret, opts.json) };
|
|
1927
|
+
// Rodadas finais 2 e 3 do cli#81: o `--env-file` é lido e conferido AQUI,
|
|
1928
|
+
// antes do catálogo e da pergunta do `pickProduct`, da pergunta do
|
|
1929
|
+
// `checkSchemaGate` e do `pickTarget`, da lista de servidores, do build e
|
|
1930
|
+
// do upload. Um arquivo fora de UTF-8 sai como `invalid_encoding` antes de
|
|
1931
|
+
// tudo isso. As variáveis só são montadas depois do `pickTarget`, porque os
|
|
1932
|
+
// avisos dependem do alvo. Com `--target workspace` o arquivo não é lido: o
|
|
1933
|
+
// workspace o descarta, com o aviso do contrato de alvo. Sem `--target`, o
|
|
1934
|
+
// menu ainda pode escolher o workspace, e então o arquivo foi conferido à toa.
|
|
1935
|
+
const envFileTexto = opts.envFile && opts.target !== 'workspace'
|
|
1936
|
+
? await lerEnvFileDoDeploy(path.resolve(cwd, opts.envFile), opts.json)
|
|
1937
|
+
: undefined;
|
|
1570
1938
|
const productSlug = await pickProduct(opts, nonInteractive, cwd);
|
|
1571
1939
|
// GAP-M1-5 — gate de schema: detecta drift antes de prosseguir.
|
|
1572
1940
|
const canDeploy = await checkSchemaGate(cwd, opts);
|
|
1573
1941
|
if (!canDeploy) {
|
|
1574
1942
|
safeExit(1);
|
|
1575
1943
|
}
|
|
1576
|
-
|
|
1944
|
+
// Protocolo de docs v1 (decisão do dono 2026-10-03): `docs check` antes do
|
|
1945
|
+
// deploy SÓ AVISA — nunca bloqueia, em nenhum ambiente.
|
|
1946
|
+
let docsCheck;
|
|
1947
|
+
if (!opts.semDocs) {
|
|
1948
|
+
try {
|
|
1949
|
+
const { deployDocsPrecheck } = await import('./docs-produto.js');
|
|
1950
|
+
docsCheck = await deployDocsPrecheck(cwd, (opts.env ?? 'dev').toLowerCase(), opts.json);
|
|
1951
|
+
}
|
|
1952
|
+
catch {
|
|
1953
|
+
docsCheck = undefined;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
// O `runtime` do config serve a duas coisas aqui: recomendar o alvo certo no
|
|
1957
|
+
// menu (que só abre em modo interativo e sem `--target`) e o gate de contrato
|
|
1958
|
+
// logo abaixo. Uma leitura só, e só vale se o config é DESTE produto.
|
|
1959
|
+
const runtimeDoConfig = runtimeDoConfigDesteProduto(await loadLocalConfig(cwd), opts.product);
|
|
1960
|
+
const target = await pickTarget(opts, nonInteractive, runtimeDoConfig);
|
|
1961
|
+
// bug_a8375858 — GATE DE CONTRATO `target × runtime × flags`.
|
|
1962
|
+
//
|
|
1963
|
+
// Roda aqui, entre a escolha do alvo e QUALQUER trabalho caro (build,
|
|
1964
|
+
// empacotamento, signed URL, upload pro GCS, POST no Core). Era exatamente
|
|
1965
|
+
// esse o custo do bug: `--target workspace` com tarball de 20 MB subia
|
|
1966
|
+
// ~35 MB pro GCS e só então voltava um 413 que falava de tamanho quando o
|
|
1967
|
+
// problema era o TIPO do alvo.
|
|
1968
|
+
//
|
|
1969
|
+
// O `runtime` já vinha no neetru.config.json e já era lido aqui (o
|
|
1970
|
+
// `loadLocalConfig` devolve o JSON cru) — só não era olhado. Ausente =
|
|
1971
|
+
// nenhuma opinião; o gate não inventa bloqueio pra repo antigo.
|
|
1972
|
+
{
|
|
1973
|
+
const veredito = checkDeployTargetContract({
|
|
1974
|
+
target,
|
|
1975
|
+
runtime: runtimeDoConfig,
|
|
1976
|
+
flags: {
|
|
1977
|
+
artifact: opts.artifact,
|
|
1978
|
+
artifactUrl: opts.artifactUrl,
|
|
1979
|
+
artifactSha256: opts.artifactSha256,
|
|
1980
|
+
image: opts.image,
|
|
1981
|
+
registry: opts.registry,
|
|
1982
|
+
registryRepo: opts.registryRepo,
|
|
1983
|
+
buildLocal: opts.buildLocal,
|
|
1984
|
+
server: opts.server,
|
|
1985
|
+
port: opts.port,
|
|
1986
|
+
stack: opts.stack,
|
|
1987
|
+
envFile: opts.envFile,
|
|
1988
|
+
domain: opts.domain,
|
|
1989
|
+
localArtifact: opts.localArtifact,
|
|
1990
|
+
},
|
|
1991
|
+
});
|
|
1992
|
+
for (const aviso of veredito.warnings) {
|
|
1993
|
+
// Em `--json` o stdout é contrato de 1 linha — aviso vai pro stderr
|
|
1994
|
+
// (bug_6d92ef71: calar por completo em modo máquina é pior).
|
|
1995
|
+
if (opts.json)
|
|
1996
|
+
console.error(aviso);
|
|
1997
|
+
else
|
|
1998
|
+
log.warn(aviso);
|
|
1999
|
+
}
|
|
2000
|
+
if (veredito.errors.length > 0) {
|
|
2001
|
+
const primeiro = veredito.errors[0];
|
|
2002
|
+
// `deployFail` emite 1 erro e sai; os demais entram no corpo pra que uma
|
|
2003
|
+
// rodada só mostre tudo que está errado (meio-conserto é não contar os
|
|
2004
|
+
// outros e o dev voltar aqui amanhã).
|
|
2005
|
+
const extras = veredito.errors
|
|
2006
|
+
.slice(1)
|
|
2007
|
+
.map((e) => `\n\nTambém: ${e.message}`)
|
|
2008
|
+
.join('');
|
|
2009
|
+
deployFail(opts.json, primeiro.code, primeiro.message + extras);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1577
2012
|
// Caminho workspace (Fase 1 do Fluxo de Ambientes) — sem server/tar/upload.
|
|
1578
2013
|
if (target === 'workspace') {
|
|
1579
2014
|
await runWorkspaceDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive);
|
|
@@ -1581,9 +2016,21 @@ export async function runDeploy(opts) {
|
|
|
1581
2016
|
}
|
|
1582
2017
|
// Caminho cloud-run (Fase 3) — imagem de container, serviço dedicado.
|
|
1583
2018
|
if (target === 'cloud-run') {
|
|
1584
|
-
await runCloudRunDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive);
|
|
2019
|
+
await runCloudRunDeploy(opts, productSlug, (opts.env ?? 'dev').toLowerCase(), cwd, nonInteractive, envFileTexto);
|
|
1585
2020
|
return;
|
|
1586
2021
|
}
|
|
2022
|
+
// F-11: env name pra GCS path (`gs://neetru-deploy-artifacts/<product>/<env>/…`).
|
|
2023
|
+
// Default 'dev' quando não definido — alinhado com o middleware OAuth.
|
|
2024
|
+
const envName = (opts.env ?? 'dev').toLowerCase();
|
|
2025
|
+
// EnvVars. Rodadas finais 2 e 3 do cli#81: o `--env-file` era lido DEPOIS
|
|
2026
|
+
// de `resolveArtifact`. Um arquivo fora de UTF-8 só era recusado depois do
|
|
2027
|
+
// build interno (que recusava no stderr, e o `deploy --json` ficava sem a
|
|
2028
|
+
// linha JSON) ou depois de o pacote em cache subir ao GCS. Agora o começo de
|
|
2029
|
+
// `runDeploy` o lê e confere; aqui só se montam as variáveis.
|
|
2030
|
+
let envVars;
|
|
2031
|
+
if (envFileTexto !== undefined) {
|
|
2032
|
+
envVars = envVarsDoEnvFile(envFileTexto, target, opts.json, envName);
|
|
2033
|
+
}
|
|
1587
2034
|
let serverChoice = null;
|
|
1588
2035
|
if (target === 'vm') {
|
|
1589
2036
|
serverChoice = await pickServer(opts, nonInteractive);
|
|
@@ -1594,20 +2041,27 @@ export async function runDeploy(opts) {
|
|
|
1594
2041
|
// motivou o guard antigo (Core rejeitava sem --port); o Core agora ignora
|
|
1595
2042
|
// porta em deploy VM, então exigi-la aqui só atrapalharia scripts/CI.
|
|
1596
2043
|
// --port continua aceito (compat) mas é ignorado com WARN — ver abaixo.
|
|
1597
|
-
|
|
1598
|
-
|
|
2044
|
+
//
|
|
2045
|
+
// bug_6d92ef71 (low): o `!opts.json` calava o aviso POR COMPLETO em modo
|
|
2046
|
+
// machine-readable — nem no stdout (certo, quebraria o contrato de 1 linha
|
|
2047
|
+
// JSON do --json), nem em canal nenhum. Quem roda via script nunca
|
|
2048
|
+
// descobria que --port foi descartado. Mantém o aviso humano em stdout no
|
|
2049
|
+
// modo interativo e manda a MESMA informação pro stderr em --json — stderr
|
|
2050
|
+
// não faz parte do contrato de saída parseável, então não quebra nada.
|
|
2051
|
+
// Mensagem também deixou de dizer "staging/prod": o guard dispara pra
|
|
2052
|
+
// QUALQUER ambiente em target=vm (inclusive dev).
|
|
2053
|
+
if (target === 'vm' && opts.port) {
|
|
2054
|
+
const portIgnoredMsg = 'A porta é gerenciada pelo Core em deploy VM (aloca por produto/ambiente/servidor); --port é ignorado.';
|
|
2055
|
+
if (opts.json) {
|
|
2056
|
+
console.error(portIgnoredMsg);
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
log.warn(portIgnoredMsg);
|
|
2060
|
+
}
|
|
1599
2061
|
}
|
|
1600
2062
|
const { domain } = await pickDomain(opts, nonInteractive, productSlug);
|
|
1601
|
-
// F-11: env name pra GCS path (`gs://neetru-deploy-artifacts/<product>/<env>/…`).
|
|
1602
|
-
// Default 'dev' quando não definido — alinhado com o middleware OAuth.
|
|
1603
|
-
const envName = (opts.env ?? 'dev').toLowerCase();
|
|
1604
2063
|
// Build / artifact (default: faz upload pra GCS via signed URL).
|
|
1605
2064
|
const artifact = await resolveArtifact(opts, productSlug, envName, cwd);
|
|
1606
|
-
// EnvVars.
|
|
1607
|
-
let envVars;
|
|
1608
|
-
if (opts.envFile) {
|
|
1609
|
-
envVars = await loadEnvFile(path.resolve(cwd, opts.envFile), target, opts.json);
|
|
1610
|
-
}
|
|
1611
2065
|
// Confirmação interativa.
|
|
1612
2066
|
if (!nonInteractive) {
|
|
1613
2067
|
log.heading('Resumo');
|
|
@@ -1616,7 +2070,13 @@ export async function runDeploy(opts) {
|
|
|
1616
2070
|
// no resumo — só cloud-run/workspace mostravam. `ambiente` é o parâmetro que
|
|
1617
2071
|
// decide se secrets/rota de prod são tocados; tem que aparecer. Em prod,
|
|
1618
2072
|
// destacado (log.warn) pra não passar batido num Enter.
|
|
1619
|
-
|
|
2073
|
+
// 2026-10-12: era `envName === 'prod'`, mas `isProdEnv` (logo acima, e a
|
|
2074
|
+
// guarda que decide o step-up MFA) aceita TAMBEM `production`. Entao um
|
|
2075
|
+
// `--env production` passava pelo gate de MFA como producao e aparecia no
|
|
2076
|
+
// resumo como ambiente qualquer — sem o destaque, com o Enter do operador
|
|
2077
|
+
// no meio. Conserto pela metade: a mesma pergunta respondida de dois jeitos
|
|
2078
|
+
// no mesmo arquivo.
|
|
2079
|
+
if (isProdEnv(envName)) {
|
|
1620
2080
|
log.warn(` ambiente: ${envName} ← PRODUÇÃO`);
|
|
1621
2081
|
}
|
|
1622
2082
|
else {
|
|
@@ -1660,6 +2120,14 @@ export async function runDeploy(opts) {
|
|
|
1660
2120
|
artifactSha256: artifact.artifactSha256,
|
|
1661
2121
|
artifactSizeBytes: artifact.artifactSizeBytes,
|
|
1662
2122
|
};
|
|
2123
|
+
// bug_678ddc98: standalone aninhado de monorepo — o agente não tem como
|
|
2124
|
+
// inferir `apps/web/server.js` olhando a raiz do tarball. Ver
|
|
2125
|
+
// `entrypointParaDeploy` (e, no PR, o que ainda falta do lado do Core).
|
|
2126
|
+
const entrypointAninhado = entrypointParaDeploy(artifact.manifest);
|
|
2127
|
+
if (entrypointAninhado) {
|
|
2128
|
+
body.entrypoint = entrypointAninhado;
|
|
2129
|
+
log.dim(` entrypoint: ${entrypointAninhado} (standalone aninhado — monorepo)`);
|
|
2130
|
+
}
|
|
1663
2131
|
if (target === 'vm') {
|
|
1664
2132
|
body.serverId = serverChoice.id;
|
|
1665
2133
|
// P6 (2026-07-19): NÃO enviar porta em deploy VM — é detalhe interno do
|
|
@@ -1671,6 +2139,10 @@ export async function runDeploy(opts) {
|
|
|
1671
2139
|
body.domain = domain;
|
|
1672
2140
|
if (envVars && Object.keys(envVars).length > 0)
|
|
1673
2141
|
body.envVars = envVars;
|
|
2142
|
+
// 2.30.0: override da varredura de segredo vai NO deploy (só caminho + tipo
|
|
2143
|
+
// de padrão, nunca o valor). O Core lê do corpo cru (como prodDirectOverride).
|
|
2144
|
+
if (artifact.publicSecretScan.override)
|
|
2145
|
+
body.publicSecretOverride = artifact.publicSecretScan.override;
|
|
1674
2146
|
const mfaHeaders = await resolveDeployStepUpHeaders(opts, envName, nonInteractive);
|
|
1675
2147
|
let deploy;
|
|
1676
2148
|
const dispatchSpinner = ora({ text: 'Enviando comando ao Core…', color: 'blue' }).start();
|
|
@@ -1680,6 +2152,7 @@ export async function runDeploy(opts) {
|
|
|
1680
2152
|
headers: mfaHeaders,
|
|
1681
2153
|
body,
|
|
1682
2154
|
});
|
|
2155
|
+
mostrarAvisosDoCore(deploy.warnings, opts.json);
|
|
1683
2156
|
dispatchSpinner.succeed(`Deploy criado: ${chalk.bold(deploy.deploymentId)}`);
|
|
1684
2157
|
}
|
|
1685
2158
|
catch (err) {
|
|
@@ -1707,6 +2180,9 @@ export async function runDeploy(opts) {
|
|
|
1707
2180
|
}
|
|
1708
2181
|
safeExit(1);
|
|
1709
2182
|
}
|
|
2183
|
+
// Docs no Core (opt-in `docs.publishOnDeploy`, só produção). O pipeline já
|
|
2184
|
+
// aplicou a versão; falha aqui só vira aviso.
|
|
2185
|
+
const docsSync = await postDeployDocs(opts, cwd, envName);
|
|
1710
2186
|
// ── Fase 0 #1: probe de saúde INDEPENDENTE antes de declarar "Live" ────────
|
|
1711
2187
|
// O pipeline pode reportar `success` com o site quebrado (outage PDV
|
|
1712
2188
|
// 2026-09-07). Só declaramos "Live" se um GET real ao domínio confirmar 200 +
|
|
@@ -1724,6 +2200,8 @@ export async function runDeploy(opts) {
|
|
|
1724
2200
|
url: final.domain ? `https://${final.domain}` : null,
|
|
1725
2201
|
artifactUrl: artifact.artifactUrl,
|
|
1726
2202
|
steps: final.steps,
|
|
2203
|
+
publicSecretScan: artifact.publicSecretScan,
|
|
2204
|
+
...(docsCheck || docsSync ? { docs: { check: docsCheck ?? null, sync: docsSync ?? null } } : {}),
|
|
1727
2205
|
};
|
|
1728
2206
|
if (opts.skipHealthProbe) {
|
|
1729
2207
|
if (opts.json) {
|