@microteia/console 2.10.0 → 2.18.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/CONSOLE-VERSIONS.md +49 -0
- package/README.md +1 -1
- package/lib/console/appliedFunctions.d.ts +79 -0
- package/lib/console/appliedFunctions.js +88 -0
- package/lib/console/backendResidue.d.ts +55 -0
- package/lib/console/backendResidue.js +53 -0
- package/lib/console/customFunctions.d.ts +51 -1
- package/lib/console/customFunctions.js +94 -1
- package/lib/console/deploy.js +17 -7
- package/lib/console/environment.d.ts +7 -6
- package/lib/console/firestoreRest.d.ts +27 -0
- package/lib/console/firestoreRest.js +23 -0
- package/lib/console/functionGate.d.ts +198 -0
- package/lib/console/functionGate.js +400 -0
- package/lib/console/provision.d.ts +26 -13
- package/lib/console/provision.js +300 -57
- package/lib/kernel/backendShape.d.ts +27 -0
- package/lib/kernel/backendShape.js +38 -0
- package/lib/shared/paths.d.ts +1 -1
- package/lib/shared/paths.js +1 -1
- package/lib/vendor/packageTemplates.d.ts +12 -1
- package/lib/vendor/packageTemplates.js +42 -2
- package/package.json +1 -1
package/lib/console/provision.js
CHANGED
|
@@ -2,7 +2,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
2
2
|
import { hasFlag, requiredFlag, flagValue } from '../shared/args.js';
|
|
3
3
|
import { run } from '../shared/run.js';
|
|
4
4
|
import { blank, bullets, failure, heading, info, step, success, warn } from '../shared/ui.js';
|
|
5
|
-
import { backendFingerprint, claimedIndexes, claimedParameters, functionClaimProblems, hasBackendRequirement, indexKey, } from '../kernel/backendShape.js';
|
|
5
|
+
import { backendFingerprint, baselineClaimProblems, claimedIndexes, claimedParameters, functionClaimProblems, hasBackendRequirement, indexKey, sharedClaimProblems, } from '../kernel/backendShape.js';
|
|
6
6
|
import { convergeIndexes, describeIndex, parseIndexState, readIndexFile, serializeIndexState, writeIndexFile, } from './backendIndexes.js';
|
|
7
7
|
import { officeEnvPath, stageOfficeEnv } from './backendEnv.js';
|
|
8
8
|
import { installPath, readInstalledManifest } from './unpack.js';
|
|
@@ -14,8 +14,11 @@ import { clearRegionOrphans, listDeployedFunctions } from './regionOrphans.js';
|
|
|
14
14
|
import { outdatedByStamp } from './functionsUpToDate.js';
|
|
15
15
|
import { checkCallables, invokerGrantCommand } from './callableAccess.js';
|
|
16
16
|
import { asString, asTimestamp, patchMapField } from './firestoreRest.js';
|
|
17
|
+
import { writeBackendResidue } from './backendResidue.js';
|
|
17
18
|
import { listTenants, readInstalledExtensions } from './installedExtensions.js';
|
|
18
19
|
import { gatherCustomFunctions, readOfficeFunctions } from './customFunctions.js';
|
|
20
|
+
import { describeMisgated, misgatedTriggers, singleClaimants, ungatedTriggerProblems, unguardedCallableProblems, unscopedTriggerProblems, } from './functionGate.js';
|
|
21
|
+
import { appliedAfterRun, classifyApplied, describeApplied, parseAppliedFunctions, serializeAppliedFunctions, } from './appliedFunctions.js';
|
|
19
22
|
/**
|
|
20
23
|
* O que cada escritório do projeto instalou e exige do backend.
|
|
21
24
|
*
|
|
@@ -23,13 +26,17 @@ import { gatherCustomFunctions, readOfficeFunctions } from './customFunctions.js
|
|
|
23
26
|
* contra `manifest.functions` antes de qualquer deploy. Nome de function fora do pacote é recusado com
|
|
24
27
|
* explicação; não há injeção possível, porque o código é sempre o do pacote.
|
|
25
28
|
*/
|
|
26
|
-
export const
|
|
29
|
+
export const surveyInstallations = async (projectId) => {
|
|
27
30
|
const tenants = await listTenants(projectId);
|
|
28
31
|
const claims = [];
|
|
32
|
+
const installedIds = new Set();
|
|
33
|
+
let extensions = 0;
|
|
29
34
|
for (const tenant of tenants) {
|
|
30
35
|
for (const extension of await readInstalledExtensions(projectId, tenant.id)) {
|
|
36
|
+
extensions += 1;
|
|
31
37
|
if (!extension.active)
|
|
32
38
|
continue;
|
|
39
|
+
installedIds.add(extension.id);
|
|
33
40
|
const backend = extension.manifest?.backend;
|
|
34
41
|
if (!hasBackendRequirement(backend))
|
|
35
42
|
continue;
|
|
@@ -45,7 +52,12 @@ export const readInstalledClaims = async (projectId) => {
|
|
|
45
52
|
});
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
tenants: tenants.length,
|
|
57
|
+
extensions,
|
|
58
|
+
claims: claims.sort((a, b) => (a.pluginId < b.pluginId ? -1 : a.pluginId > b.pluginId ? 1 : 0)),
|
|
59
|
+
installedIds,
|
|
60
|
+
};
|
|
49
61
|
};
|
|
50
62
|
const uniqueByPlugin = (claims) => {
|
|
51
63
|
const seen = new Set();
|
|
@@ -56,7 +68,6 @@ const uniqueByPlugin = (claims) => {
|
|
|
56
68
|
return true;
|
|
57
69
|
});
|
|
58
70
|
};
|
|
59
|
-
export const parseAppliedFunctions = (value) => (value ?? '').split(',').map((name) => name.trim()).filter((name) => name !== '');
|
|
60
71
|
/**
|
|
61
72
|
* O motor: alinha o projeto Firebase com o que as extensões instaladas exigem, e carimba nelas o
|
|
62
73
|
* estado. Um motor só, chamado pelo `deploy` no fim e pelo `provision` sozinho — duas implementações
|
|
@@ -77,12 +88,26 @@ export const provisionBackend = async (options) => {
|
|
|
77
88
|
*/
|
|
78
89
|
const office = await readOfficeFunctions(root);
|
|
79
90
|
const deployable = [...manifest.functions, ...office.all];
|
|
80
|
-
const
|
|
81
|
-
const
|
|
91
|
+
const survey = await surveyInstallations(projectId);
|
|
92
|
+
const claims = survey.claims;
|
|
93
|
+
const missing = functionClaimProblems(claims, deployable);
|
|
94
|
+
const overlapping = baselineClaimProblems(claims, manifest.baselineFunctions ?? []);
|
|
95
|
+
// A comum do escritório tem o MESMO desfecho da function da plataforma, e por isso a mesma recusa: o
|
|
96
|
+
// `deploy` a sobe sempre, então reivindicá-la não muda nada — até o dia em que `--prune` a apaga.
|
|
97
|
+
const appropriated = sharedClaimProblems(claims, office.shared);
|
|
98
|
+
// O portão aberto para o id de OUTRA extensão. Só aqui se pode perguntar: é a reivindicação que diz de
|
|
99
|
+
// quem a function é, e ela vem do banco do escritório — o `deploy` não a tem.
|
|
100
|
+
const misgated = misgatedTriggers(office.sources, singleClaimants(claims));
|
|
101
|
+
const problems = [
|
|
102
|
+
...missing,
|
|
103
|
+
...overlapping,
|
|
104
|
+
...appropriated,
|
|
105
|
+
...misgated.map(describeMisgated),
|
|
106
|
+
];
|
|
82
107
|
const rejected = new Set();
|
|
83
|
-
if (
|
|
108
|
+
if (missing.length > 0) {
|
|
84
109
|
warn('some extensions ask for backend artifacts that are nowhere to be found:');
|
|
85
|
-
bullets(
|
|
110
|
+
bullets(missing);
|
|
86
111
|
// A mensagem sozinha é um beco sem saída: ela diz que o nome não existe, e não onde ele deveria
|
|
87
112
|
// existir. As duas respostas possíveis são estas, e a segunda é a que mais acontece — a function é
|
|
88
113
|
// da extensão, o autor a declarou, e ninguém a exportou do `index.ts` do diretório dela.
|
|
@@ -93,18 +118,92 @@ export const provisionBackend = async (options) => {
|
|
|
93
118
|
]);
|
|
94
119
|
info(` this installation currently exports ${office.all.length} function(s) of its own`
|
|
95
120
|
+ `${office.all.length === 0 ? '' : `: ${office.all.join(', ')}`}`);
|
|
121
|
+
}
|
|
122
|
+
if (overlapping.length > 0) {
|
|
123
|
+
warn('some extensions claim a function that belongs to the platform itself:');
|
|
124
|
+
bullets(overlapping);
|
|
125
|
+
info(' every "microteia deploy" puts those up, extension or no extension, so claiming one changes');
|
|
126
|
+
info(' nothing while it is installed — and takes a working trigger out of this project the day it');
|
|
127
|
+
info(' is uninstalled. Rename the extension\'s own function, or drop the name from its');
|
|
128
|
+
info(' manifest.backend.functions.');
|
|
129
|
+
}
|
|
130
|
+
if (appropriated.length > 0) {
|
|
131
|
+
warn('some extensions claim a function that is shared by this whole installation:');
|
|
132
|
+
bullets(appropriated);
|
|
133
|
+
info(' functions/ next to microteia.yaml is the place for a function that serves SEVERAL');
|
|
134
|
+
info(' extensions, so it has no owner and every "microteia deploy" puts it up. Claiming one changes');
|
|
135
|
+
info(' nothing while the extension is installed — and the day it is uninstalled the function turns');
|
|
136
|
+
info(' into an orphan, --prune deletes it, and the next deploy puts it back. Whoever else depended');
|
|
137
|
+
info(' on it loses their backend in a window nothing explains. Move the function into');
|
|
138
|
+
info(' extensions/<folder>/functions/ if it is really this extension\'s, drop the name from its');
|
|
139
|
+
info(' manifest.backend.functions, or — if several extensions need it — name them in "microteia" ->');
|
|
140
|
+
info(' "serves" in functions/package.json, which is how a shared function gets withdrawn.');
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* O gatilho sem portão é RELATADO aqui e RECUSADO na montagem (`functionProblems`), e a assimetria é
|
|
144
|
+
* deliberada: este é o único comando capaz de RETIRAR uma function do projeto. Recusar aqui trancaria
|
|
145
|
+
* `--prune` — o comando que apaga a órfã — por causa de uma fonte que ele não precisa ler. A recusa
|
|
146
|
+
* acontece onde a fonte de fato entra no pacote, e aí ela não tranca a saída de ninguém.
|
|
147
|
+
*/
|
|
148
|
+
const ungated = [
|
|
149
|
+
...ungatedTriggerProblems(office.sources),
|
|
150
|
+
...unscopedTriggerProblems(office.sources),
|
|
151
|
+
...unguardedCallableProblems(office.sources),
|
|
152
|
+
];
|
|
153
|
+
if (ungated.length > 0) {
|
|
154
|
+
warn(`${ungated.length} function(s) written in this installation act outside any office's`
|
|
155
|
+
+ ' installation:');
|
|
156
|
+
bullets(ungated);
|
|
157
|
+
info(' they are refused the moment something tries to deploy them. Until then they keep acting on');
|
|
158
|
+
info(' every office in this project.');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A comum SEM declaração de quem serve. Ela continua subindo no `deploy`, sempre, e nada a retira — é
|
|
162
|
+
* o comportamento anterior, mantido para não quebrar instalação nenhuma.
|
|
163
|
+
*
|
|
164
|
+
* O que muda é que ele deixa de ser invisível. Era esse o defeito: quem herdasse a instalação não tinha
|
|
165
|
+
* como saber que aquelas functions estavam fora do mecanismo de retirada, e a única forma de descobrir
|
|
166
|
+
* era um gatilho disparando anos depois de a última extensão que o usava ter saído.
|
|
167
|
+
*/
|
|
168
|
+
if (office.unserved.length > 0) {
|
|
169
|
+
blank();
|
|
170
|
+
info(` ${office.unserved.length} shared function(s) do not say which extensions they serve:`);
|
|
171
|
+
bullets(office.unserved);
|
|
172
|
+
info(' they go up with every "microteia deploy" and nothing here withdraws them — not even --prune.');
|
|
173
|
+
info(' Name the extensions each one exists for in "microteia" -> "serves" in functions/package.json,');
|
|
174
|
+
info(' and they are deployed only while one of those is installed, and removed when none is.');
|
|
175
|
+
}
|
|
176
|
+
if (misgated.length > 0) {
|
|
177
|
+
warn('some extensions gate a trigger for an extension id that is not their own:');
|
|
178
|
+
bullets(misgated.map(describeMisgated));
|
|
179
|
+
info(' it is what a copied function leaves behind. The trigger deploys, fires on every write, and');
|
|
180
|
+
info(' the handler never runs — the gate asks about an extension nobody installed. Fix the id in');
|
|
181
|
+
info(' the forInstalledExtension call.');
|
|
182
|
+
}
|
|
183
|
+
if (problems.length > 0) {
|
|
96
184
|
for (const claim of claims) {
|
|
97
185
|
if (problems.some((problem) => problem.includes(claim.pluginId)))
|
|
98
186
|
rejected.add(claim.pluginId);
|
|
99
187
|
}
|
|
188
|
+
// O dono de um portão errado é pendurado por IDENTIDADE, e não por o id dele aparecer no texto: a
|
|
189
|
+
// mensagem cita DOIS ids, e casar por substring penduraria também a extensão cujo id foi copiado.
|
|
190
|
+
for (const entry of misgated)
|
|
191
|
+
rejected.add(entry.owner);
|
|
100
192
|
}
|
|
101
193
|
const valid = uniqueByPlugin(claims.filter((claim) => !rejected.has(claim.pluginId)));
|
|
102
194
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
195
|
+
* O nome que uma reivindicação RECUSADA carrega. Ele não entra em `wantedFunctions` — não há como
|
|
196
|
+
* deployá-lo —, mas também não pode virar órfão: a extensão que o pede continua instalada, e o que
|
|
197
|
+
* falhou foi resolver a declaração dela. Sem esta lista, um export renomeado em
|
|
198
|
+
* `extensions/<x>/functions/index.ts` fazia `--prune` apagar a function de uma extensão viva.
|
|
107
199
|
*/
|
|
200
|
+
const unresolved = new Map();
|
|
201
|
+
for (const claim of claims) {
|
|
202
|
+
if (!rejected.has(claim.pluginId))
|
|
203
|
+
continue;
|
|
204
|
+
for (const name of claim.backend.functions ?? [])
|
|
205
|
+
unresolved.set(name, claim.pluginId);
|
|
206
|
+
}
|
|
108
207
|
if (valid.length === 0) {
|
|
109
208
|
info(' no installed extension asks for backend artifacts');
|
|
110
209
|
}
|
|
@@ -113,14 +212,60 @@ export const provisionBackend = async (options) => {
|
|
|
113
212
|
bullets(valid.map((claim) => `${claim.name} ${claim.version} (${claim.pluginId})`));
|
|
114
213
|
}
|
|
115
214
|
const settled = await settleIndexes(options, valid);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
215
|
+
/**
|
|
216
|
+
* NÃO existe mais um desvio de "nenhuma extensão exige nada" aqui — e é a correção do defeito que
|
|
217
|
+
* este comando tinha justamente no caso mais comum. Desinstalar a ÚLTIMA extensão com backend caía
|
|
218
|
+
* naquele desvio, e o desvio devolvia antes do cálculo de órfã: a Cloud Function dela continuava no
|
|
219
|
+
* ar para sempre, `--prune` não a alcançava, e a lista de aplicadas era regravada intacta. O sintoma
|
|
220
|
+
* era um gatilho `onDocumentWritten` de uma extensão que ninguém tem mais, disparando sobre os dados
|
|
221
|
+
* do escritório a cada gravação.
|
|
222
|
+
*
|
|
223
|
+
* O índice já havia sido salvo dessa mesma armadilha — foi por isso que `settleIndexes` subiu para
|
|
224
|
+
* antes do desvio. A function ficou para trás, e a lição é que o desvio é que estava errado: o que
|
|
225
|
+
* ele pulava não era "trabalho inútil", era a retirada.
|
|
226
|
+
*
|
|
227
|
+
* O que sobrou dele são duas guardas pontuais mais abaixo: não perguntar ao projeto o que ele roda
|
|
228
|
+
* quando não há nada a conferir, e não anunciar que "as 0 functions já estão deployadas".
|
|
229
|
+
*/
|
|
230
|
+
/**
|
|
231
|
+
* A function COMUM que declara quem serve entra aqui, e é o que a tira da invisibilidade.
|
|
232
|
+
*
|
|
233
|
+
* Ela não tem dono que possa reivindicá-la — nenhuma extensão pode —, então ela ficava fora do
|
|
234
|
+
* mecanismo por inteiro: nunca entrava no registro de aplicadas, nunca virava órfã, nunca era
|
|
235
|
+
* relatada, e o `--prune` não a alcançava. Com a declaração, a pergunta passa a ter resposta: ela é
|
|
236
|
+
* devida enquanto ALGUMA das extensões que ela serve estiver instalada em algum escritório do projeto.
|
|
237
|
+
*
|
|
238
|
+
* "Basta uma" é a única regra possível: a function existe porque mais de uma extensão precisa dela.
|
|
239
|
+
* Exigir todas faria desinstalar uma desligar o backend das outras.
|
|
240
|
+
*/
|
|
241
|
+
const sharedWanted = office.served.filter((entry) => entry.serves.some((id) => survey.installedIds.has(id)));
|
|
242
|
+
const sharedWantedNames = new Set(sharedWanted.map((entry) => entry.name));
|
|
243
|
+
const wantedFunctions = deployable.filter((name) => sharedWantedNames.has(name)
|
|
244
|
+
|| valid.some((claim) => (claim.backend.functions ?? []).includes(name)));
|
|
245
|
+
/** Quem reivindica cada nome AGORA — é com este dono que o registro é reconciliado. */
|
|
246
|
+
const owners = new Map();
|
|
247
|
+
for (const claim of valid) {
|
|
248
|
+
for (const name of claim.backend.functions ?? [])
|
|
249
|
+
owners.set(name, claim.pluginId);
|
|
120
250
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
251
|
+
// A comum tem VÁRIOS donos, e o registro guarda a lista declarada em vez de eleger um deles: é ela que
|
|
252
|
+
// explica, meses depois, por que aquela function estava ali — e por que ela deixou de estar.
|
|
253
|
+
for (const entry of sharedWanted)
|
|
254
|
+
owners.set(entry.name, entry.serves.join('+'));
|
|
255
|
+
const applied = parseAppliedFunctions(options.environment.backend.functions);
|
|
256
|
+
const appliedNames = new Set(applied.map((entry) => entry.name));
|
|
257
|
+
const classification = classifyApplied({
|
|
258
|
+
applied,
|
|
259
|
+
wanted: wantedFunctions,
|
|
260
|
+
baseline: manifest.baselineFunctions ?? [],
|
|
261
|
+
unresolved,
|
|
262
|
+
});
|
|
263
|
+
// Os do diretório comum entram: são eles que as functions comuns servidas carregam, e um parâmetro
|
|
264
|
+
// que ninguém acompanha é um valor que nunca chega à function que o lê.
|
|
265
|
+
const trackedParameters = [...new Set([
|
|
266
|
+
...claimedParameters(valid),
|
|
267
|
+
...(office.sources.find((source) => source.shared)?.parameters ?? []).map((p) => p.name),
|
|
268
|
+
])].sort();
|
|
124
269
|
// A raiz, e não o `install/`: a fonte é o arquivo que o escritório edita ao lado do `microteia.yaml`.
|
|
125
270
|
const filledParameters = await readBackendEnv(dirname(options.environmentFile), projectId);
|
|
126
271
|
const recordedDigests = parseParameterState(options.environment.backend.parameters);
|
|
@@ -137,13 +282,46 @@ export const provisionBackend = async (options) => {
|
|
|
137
282
|
// instalação fica sem nenhum comando que o aplique, e a feature falha no runtime.
|
|
138
283
|
const changed = changedParameters(recordedDigests, currentDigests, trackedParameters);
|
|
139
284
|
const staleFunctions = new Set(functionsDeclaringParameters(valid, changed)
|
|
140
|
-
.filter((name) => wantedFunctions.includes(name) &&
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
285
|
+
.filter((name) => wantedFunctions.includes(name) && appliedNames.has(name)));
|
|
286
|
+
/**
|
|
287
|
+
* A comum que agora sobe pelo provisionamento também precisa voltar à fila quando um parâmetro muda.
|
|
288
|
+
* Antes ela subia a cada `deploy`, e isso mascarava a pergunta.
|
|
289
|
+
*
|
|
290
|
+
* A granularidade é o DIRETÓRIO, e não a function: `functions/` declara os `defineString` num conjunto
|
|
291
|
+
* só, sem dizer qual function lê qual. Redeployar todas as comuns servidas quando um parâmetro do
|
|
292
|
+
* diretório muda erra para o lado de fazer o que é devido — o inverso deixaria uma delas rodando com o
|
|
293
|
+
* valor antigo, que é o defeito que este mecanismo existe para fechar.
|
|
294
|
+
*/
|
|
295
|
+
const sharedSource = office.sources.find((source) => source.shared);
|
|
296
|
+
const sharedParameters = new Set((sharedSource?.parameters ?? []).map((parameter) => parameter.name));
|
|
297
|
+
if (changed.some((name) => sharedParameters.has(name))) {
|
|
298
|
+
for (const entry of sharedWanted) {
|
|
299
|
+
if (appliedNames.has(entry.name))
|
|
300
|
+
staleFunctions.add(entry.name);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Uma pergunta só ao projeto, e duas decisões a partir dela: o que ele já roda, e qual cópia ficou
|
|
305
|
+
* numa região abandonada. Perguntar duas vezes não é caro, é ambíguo — as duas respostas podem
|
|
306
|
+
* divergir, e aí a decisão passa a depender de qual delas o código olhou.
|
|
307
|
+
*
|
|
308
|
+
* E ela é feita só quando há o que decidir. Sem o desvio lá em cima, este caminho passou a ser
|
|
309
|
+
* percorrido também pelo `provision` que roda quando não sobrou extensão nenhuma — e ali não há
|
|
310
|
+
* function para conferir nem para subir, então um `functions:list` seria pagar segundos por uma
|
|
311
|
+
* resposta que ninguém lê.
|
|
312
|
+
*/
|
|
313
|
+
let asked;
|
|
314
|
+
const projectFunctions = async () => {
|
|
315
|
+
if (asked === undefined)
|
|
316
|
+
asked = await listDeployedFunctions(projectId);
|
|
317
|
+
return asked;
|
|
318
|
+
};
|
|
144
319
|
// O que a lista de nomes nunca respondeu: a function aplicada que sumiu do projeto, e a que segue
|
|
145
320
|
// lá carregando o carimbo de um código anterior. As duas voltam para a fila.
|
|
146
|
-
const
|
|
321
|
+
const appliedAndWanted = wantedFunctions.filter((name) => appliedNames.has(name));
|
|
322
|
+
const outdated = appliedAndWanted.length === 0
|
|
323
|
+
? { absent: [], stale: [] }
|
|
324
|
+
: outdatedByStamp(appliedAndWanted, await projectFunctions(), region, options.refreshedStamps);
|
|
147
325
|
for (const name of [...outdated.absent, ...outdated.stale])
|
|
148
326
|
staleFunctions.add(name);
|
|
149
327
|
if (outdated.absent.length > 0) {
|
|
@@ -180,9 +358,13 @@ export const provisionBackend = async (options) => {
|
|
|
180
358
|
info(' Cloud Function at ITS deploy, so a function already running keeps the previous one.');
|
|
181
359
|
blank();
|
|
182
360
|
}
|
|
183
|
-
const toDeploy = wantedFunctions.filter((name) => options.all === true || !
|
|
361
|
+
const toDeploy = wantedFunctions.filter((name) => options.all === true || !appliedNames.has(name) || staleFunctions.has(name));
|
|
184
362
|
if (toDeploy.length === 0) {
|
|
185
|
-
|
|
363
|
+
// Anunciar "as 0 functions já estão deployadas" seria o preço de ter tirado o desvio, e não é
|
|
364
|
+
// preciso pagá-lo: quando não há exigência nenhuma, quem já falou foi a linha lá em cima.
|
|
365
|
+
if (wantedFunctions.length > 0) {
|
|
366
|
+
success(`the ${wantedFunctions.length} function(s) these extensions need are already deployed`);
|
|
367
|
+
}
|
|
186
368
|
}
|
|
187
369
|
else {
|
|
188
370
|
// A cópia que o `firebase-functions` lê é atualizada AGORA, e não só no `deploy`: este comando é o
|
|
@@ -199,7 +381,7 @@ export const provisionBackend = async (options) => {
|
|
|
199
381
|
}
|
|
200
382
|
// A extensão que mudou de região deixa a cópia antiga para trás igual ao kernel, e ali o efeito
|
|
201
383
|
// é o mesmo: deploy recusado em toda tentativa, e gatilho disparando em duas regiões.
|
|
202
|
-
await clearRegionOrphans(projectId, toDeploy, region, options.prune,
|
|
384
|
+
await clearRegionOrphans(projectId, toDeploy, region, options.prune, await projectFunctions());
|
|
203
385
|
info(` deploying ${toDeploy.length} function(s), one at a time…`);
|
|
204
386
|
const report = await deployFunctionsOneByOne(toDeploy, {
|
|
205
387
|
projectId, cwd: install, delaySeconds, force: false,
|
|
@@ -210,7 +392,7 @@ export const provisionBackend = async (options) => {
|
|
|
210
392
|
reportNotices(report.notices);
|
|
211
393
|
await checkNewCallables([...(manifest.callables ?? []), ...office.callables], report.deployed, projectId, region);
|
|
212
394
|
}
|
|
213
|
-
const deployedNow = new Set([...
|
|
395
|
+
const deployedNow = new Set([...appliedNames, ...outcome.deployed]);
|
|
214
396
|
for (const claim of valid) {
|
|
215
397
|
const satisfied = (claim.backend.functions ?? []).every((name) => deployedNow.has(name));
|
|
216
398
|
if (satisfied) {
|
|
@@ -220,14 +402,16 @@ export const provisionBackend = async (options) => {
|
|
|
220
402
|
outcome.pending.push(claim);
|
|
221
403
|
}
|
|
222
404
|
}
|
|
223
|
-
|
|
224
|
-
outcome.orphans = orphans;
|
|
225
|
-
const remaining = await settleOrphans(options, orphans);
|
|
405
|
+
reportPreserved(classification.preserved);
|
|
406
|
+
outcome.orphans = classification.orphans.map((entry) => entry.name);
|
|
407
|
+
const remaining = await settleOrphans(options, classification.orphans, survey);
|
|
226
408
|
await recordBackendState(options, appliedAfterRun({
|
|
227
|
-
|
|
409
|
+
applied,
|
|
228
410
|
wanted: wantedFunctions,
|
|
229
411
|
deployed: outcome.deployed,
|
|
230
412
|
keptOrphans: remaining,
|
|
413
|
+
preserved: classification.preserved.map((item) => item.entry),
|
|
414
|
+
owners,
|
|
231
415
|
}), appliedParameterState({
|
|
232
416
|
recorded: recordedDigests,
|
|
233
417
|
current: currentDigests,
|
|
@@ -236,8 +420,37 @@ export const provisionBackend = async (options) => {
|
|
|
236
420
|
claims: valid,
|
|
237
421
|
}), settled.declared);
|
|
238
422
|
reportOrphanIndexes(projectId, settled.orphans, options.prune);
|
|
423
|
+
await publishResidue(projectId, remaining, settled.orphans, survey);
|
|
239
424
|
return outcome;
|
|
240
425
|
};
|
|
426
|
+
/**
|
|
427
|
+
* O que sobrou, devolvido ao banco para a aplicação poder mostrá-lo.
|
|
428
|
+
*
|
|
429
|
+
* A lista é a que sobra DEPOIS do `--prune`: o que ele apagou saiu, e o que ele não conseguiu apagar
|
|
430
|
+
* fica — é o estado do projeto agora, e não o do começo da execução.
|
|
431
|
+
*
|
|
432
|
+
* Falhar aqui não derruba o comando, e a exceção é justificada: as functions e os índices já foram
|
|
433
|
+
* aplicados, e o que ficou de fazer era um RELATO. Recusar o `provision` inteiro por causa dele mandaria
|
|
434
|
+
* quem instala rodar de novo um comando que já terminou o trabalho. Mas o aviso é dito em voz alta —
|
|
435
|
+
* calar deixaria a tela silenciosa parecendo "não há resíduo", que é a afirmação oposta.
|
|
436
|
+
*/
|
|
437
|
+
const publishResidue = async (projectId, functions, indexes, survey) => {
|
|
438
|
+
try {
|
|
439
|
+
await writeBackendResidue(projectId, {
|
|
440
|
+
scannedAt: new Date(),
|
|
441
|
+
tenants: survey.tenants,
|
|
442
|
+
extensions: survey.extensions,
|
|
443
|
+
functions: functions.map((entry) => ({ name: entry.name, owner: entry.owner ?? '' })),
|
|
444
|
+
indexes: indexes.map(describeIndex),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
warn('could not record what is left over where the application can show it:');
|
|
449
|
+
info(` ${error instanceof Error ? error.message : String(error)}`);
|
|
450
|
+
info(' the artifacts above were applied. What did not happen is the Extensions screen learning');
|
|
451
|
+
info(' about the leftovers — it keeps showing what the previous run recorded.');
|
|
452
|
+
}
|
|
453
|
+
};
|
|
241
454
|
/**
|
|
242
455
|
* O que o projeto tem de declarar de índice composto, e o que sobrou de uma extensão que saiu.
|
|
243
456
|
*
|
|
@@ -320,34 +533,74 @@ const stampApplied = async (projectId, all, claim) => {
|
|
|
320
533
|
success(`${claim.name} is ready in ${projectId}`);
|
|
321
534
|
};
|
|
322
535
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
536
|
+
* O que ninguém exige e mesmo assim NÃO é órfã.
|
|
537
|
+
*
|
|
538
|
+
* Relatar é o ponto. As duas causas são configuração errada de um lado ou do outro, e o silêncio sobre
|
|
539
|
+
* elas faria a próxima pessoa a abrir o `microteia.yaml` concluir que a lista está suja — e apagar à
|
|
540
|
+
* mão exatamente o que estas linhas protegem.
|
|
541
|
+
*/
|
|
542
|
+
const reportPreserved = (preserved) => {
|
|
543
|
+
const baseline = preserved.filter((item) => item.reason === 'baseline');
|
|
544
|
+
const unresolved = preserved.filter((item) => item.reason === 'unresolved');
|
|
545
|
+
if (baseline.length > 0) {
|
|
546
|
+
blank();
|
|
547
|
+
warn(`${baseline.length} function(s) in this installation's record belong to the platform:`);
|
|
548
|
+
bullets(baseline.map((item) => describeApplied(item.entry)));
|
|
549
|
+
info(' every "microteia deploy" puts them up, extension or no extension, so nothing here removes');
|
|
550
|
+
info(' them — not even --prune.');
|
|
551
|
+
}
|
|
552
|
+
if (unresolved.length > 0) {
|
|
553
|
+
blank();
|
|
554
|
+
warn(`${unresolved.length} function(s) belong to an extension whose declaration did not resolve:`);
|
|
555
|
+
bullets(unresolved.map((item) => `${item.entry.name} — claimed by ${item.claimant}`));
|
|
556
|
+
info(' the extension is still installed, so they are not orphans and nothing removes them.');
|
|
557
|
+
info(' Fix what is reported above and run "microteia provision" again.');
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* Function que nenhuma extensão instalada exige mais.
|
|
562
|
+
*
|
|
563
|
+
* A CERTEZA vem antes da remoção, e é dita em voz alta: a Cloud Function é do PROJETO e a instalação é
|
|
564
|
+
* do ESCRITÓRIO, então "ninguém mais exige" é uma afirmação sobre TODOS os escritórios do projeto, e
|
|
565
|
+
* não sobre o último que foi lido. A varredura é completa ou não existe — `listDocuments` pagina até o
|
|
566
|
+
* fim e qualquer leitura recusada derruba o comando lá em cima —, e é o número dito aqui que permite a
|
|
567
|
+
* quem lê conferir a afirmação em vez de acreditar nela.
|
|
568
|
+
*
|
|
569
|
+
* Apagar é destrutivo e irreversível, então o padrão RELATA e só `--prune` remove — e o que foi
|
|
570
|
+
* removido sai da lista de aplicadas.
|
|
325
571
|
*/
|
|
326
|
-
const settleOrphans = async (options, orphans) => {
|
|
572
|
+
const settleOrphans = async (options, orphans, survey) => {
|
|
327
573
|
if (orphans.length === 0)
|
|
328
574
|
return [];
|
|
329
575
|
blank();
|
|
576
|
+
warn(`${orphans.length} function(s) are deployed and no installed extension needs them:`);
|
|
577
|
+
bullets(orphans.map(describeApplied));
|
|
578
|
+
info(` read from every office in this project: ${survey.tenants} office(s), `
|
|
579
|
+
+ `${survey.extensions} installed extension(s).`);
|
|
330
580
|
if (!options.prune) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
581
|
+
// O custo mudou de descrição porque a descrição estava errada. Uma callable ociosa não custa nada;
|
|
582
|
+
// um gatilho de Firestore não fica ocioso — ele segue disparando a cada gravação do escritório,
|
|
583
|
+
// rodando o código de uma extensão que ninguém tem mais, e é esse efeito que faz a remoção valer
|
|
584
|
+
// a pena antes de "algum dia".
|
|
585
|
+
info(' a callable just sits there. A Firestore trigger does not: it keeps firing on every write,');
|
|
586
|
+
info(' running the code of an extension nobody has any more. To remove them:');
|
|
334
587
|
bullets(['microteia provision --prune']);
|
|
335
588
|
blank();
|
|
336
589
|
return orphans;
|
|
337
590
|
}
|
|
338
591
|
info(` deleting ${orphans.length} orphan function(s)…`);
|
|
339
|
-
const deleted =
|
|
340
|
-
for (const
|
|
592
|
+
const deleted = new Set();
|
|
593
|
+
for (const orphan of orphans) {
|
|
341
594
|
try {
|
|
342
|
-
await run(FIREBASE_BIN, ['functions:delete', name, '--project', options.projectId, '--region', options.region, '--force'], { cwd: options.install });
|
|
343
|
-
deleted.
|
|
595
|
+
await run(FIREBASE_BIN, ['functions:delete', orphan.name, '--project', options.projectId, '--region', options.region, '--force'], { cwd: options.install });
|
|
596
|
+
deleted.add(orphan.name);
|
|
344
597
|
}
|
|
345
598
|
catch {
|
|
346
|
-
warn(`could not delete ${name}; it stays recorded as deployed`);
|
|
599
|
+
warn(`could not delete ${orphan.name}; it stays recorded as deployed`);
|
|
347
600
|
}
|
|
348
601
|
}
|
|
349
|
-
success(`${deleted.
|
|
350
|
-
return orphans.filter((
|
|
602
|
+
success(`${deleted.size} orphan function(s) removed`);
|
|
603
|
+
return orphans.filter((orphan) => !deleted.has(orphan.name));
|
|
351
604
|
};
|
|
352
605
|
/**
|
|
353
606
|
* Índice órfão é RELATADO, nunca apagado às cegas: o `firebase deploy` não remove índice por conta, e a
|
|
@@ -380,16 +633,6 @@ const reportOrphanIndexes = (projectId, orphans, prune) => {
|
|
|
380
633
|
]);
|
|
381
634
|
blank();
|
|
382
635
|
};
|
|
383
|
-
/**
|
|
384
|
-
* A lista aplicada CONVERGE para o que ainda é exigido: o que já estava e continua sendo pedido, mais
|
|
385
|
-
* o que subiu agora, mais o órfão que não foi removido. Reescrevê-la só com o que subiu NESTA execução
|
|
386
|
-
* a esvaziava na segunda passada — e a terceira redeployava tudo, achando que nada havia sido feito.
|
|
387
|
-
*/
|
|
388
|
-
export const appliedAfterRun = (input) => [...new Set([
|
|
389
|
-
...input.alreadyApplied.filter((name) => input.wanted.includes(name)),
|
|
390
|
-
...input.deployed,
|
|
391
|
-
...input.keptOrphans,
|
|
392
|
-
])].sort();
|
|
393
636
|
/**
|
|
394
637
|
* A impressão digital do parâmetro só avança quando TODAS as functions que o carregam subiram. Gravar
|
|
395
638
|
* antes disso é o que transformaria uma falha de deploy em silêncio permanente: o valor novo ficaria
|
|
@@ -414,7 +657,7 @@ export const appliedParameterState = (input) => {
|
|
|
414
657
|
}
|
|
415
658
|
return next;
|
|
416
659
|
};
|
|
417
|
-
const recordBackendState = async (options,
|
|
660
|
+
const recordBackendState = async (options, applied, parameters, indexes) => {
|
|
418
661
|
const state = serializeIndexState(indexes);
|
|
419
662
|
/**
|
|
420
663
|
* Índice que a codificação não representa sem ambiguidade fica de fora COM AVISO, em vez de voltar do
|
|
@@ -431,7 +674,7 @@ const recordBackendState = async (options, names, parameters, indexes) => {
|
|
|
431
674
|
await writeEnvironment(options.environmentFile, {
|
|
432
675
|
...options.environment,
|
|
433
676
|
backend: {
|
|
434
|
-
functions:
|
|
677
|
+
functions: serializeAppliedFunctions(applied),
|
|
435
678
|
parameters: serializeParameterState(parameters),
|
|
436
679
|
indexes: state.value,
|
|
437
680
|
},
|
|
@@ -54,6 +54,33 @@ export declare const baselineIndexesOf: (rootIndexes: FirestoreIndexSpec[], clai
|
|
|
54
54
|
* no `provision`, contra o que veio do banco do escritório, que é dado e não código.
|
|
55
55
|
*/
|
|
56
56
|
export declare const functionClaimProblems: (claims: BackendClaim[], packagedFunctions: string[]) => string[];
|
|
57
|
+
/**
|
|
58
|
+
* Reivindicar uma function que a PLATAFORMA sobe sempre.
|
|
59
|
+
*
|
|
60
|
+
* É conferência de PROVISIONAMENTO, não de empacotamento, e a assimetria é da própria definição: no
|
|
61
|
+
* `pack` a baseline é DERIVADA (`todas − reivindicadas`), então reivindicar é o que tira o nome dela e
|
|
62
|
+
* a pergunta não existe. Do lado de quem instala a baseline já está decidida e viaja no manifesto, e
|
|
63
|
+
* aí a reivindicação de um nome dela é uma contradição com duas consequências, nenhuma visível: o
|
|
64
|
+
* `deploy` sobe a function de qualquer jeito, e a desinstalação da extensão faz `--prune` apagar um
|
|
65
|
+
* gatilho do produto num escritório que não fez nada.
|
|
66
|
+
*/
|
|
67
|
+
export declare const baselineClaimProblems: (claims: BackendClaim[], baselineFunctions: string[]) => string[];
|
|
68
|
+
/**
|
|
69
|
+
* Reivindicar uma function COMUM do escritório — a que mora em `functions/`, ao lado do
|
|
70
|
+
* `microteia.yaml`.
|
|
71
|
+
*
|
|
72
|
+
* É o mesmo defeito de reivindicar uma function da plataforma, com o mesmo par de consequências
|
|
73
|
+
* invisíveis, e por isso é conferido no mesmo lugar. A comum não tem dono por construção: ela é o lugar
|
|
74
|
+
* de quem serve várias extensões, nenhuma pode declará-la (duas reivindicando o mesmo nome é recusa), e
|
|
75
|
+
* o `deploy` a sobe SEMPRE. Reivindicá-la, então, não muda nada enquanto a extensão está instalada — e
|
|
76
|
+
* no dia em que ela sai, a function vira órfã, `--prune` a apaga, e o `deploy` seguinte a repõe. As
|
|
77
|
+
* outras extensões que dependiam dela perdem o backend numa janela que nada explica, e a fonte continua
|
|
78
|
+
* intacta em `functions/`, sem nada na árvore que sequer sugira o que aconteceu.
|
|
79
|
+
*
|
|
80
|
+
* A assimetria com o `pack` é a mesma de `baselineClaimProblems`: o pacote do kernel não sabe o que o
|
|
81
|
+
* escritório vai escrever em `functions/`, então a pergunta só existe do lado de quem instala.
|
|
82
|
+
*/
|
|
83
|
+
export declare const sharedClaimProblems: (claims: BackendClaim[], sharedFunctions: string[]) => string[];
|
|
57
84
|
/**
|
|
58
85
|
* Índice reivindicado que não existe na raiz. É conferência de EMPACOTAMENTO, não de provisionamento:
|
|
59
86
|
* a raiz é o que roda em `microteia-dev` e `curatino`, então uma declaração que não esteja lá deixaria
|
|
@@ -61,6 +61,44 @@ export const functionClaimProblems = (claims, packagedFunctions) => {
|
|
|
61
61
|
}
|
|
62
62
|
return problems;
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Reivindicar uma function que a PLATAFORMA sobe sempre.
|
|
66
|
+
*
|
|
67
|
+
* É conferência de PROVISIONAMENTO, não de empacotamento, e a assimetria é da própria definição: no
|
|
68
|
+
* `pack` a baseline é DERIVADA (`todas − reivindicadas`), então reivindicar é o que tira o nome dela e
|
|
69
|
+
* a pergunta não existe. Do lado de quem instala a baseline já está decidida e viaja no manifesto, e
|
|
70
|
+
* aí a reivindicação de um nome dela é uma contradição com duas consequências, nenhuma visível: o
|
|
71
|
+
* `deploy` sobe a function de qualquer jeito, e a desinstalação da extensão faz `--prune` apagar um
|
|
72
|
+
* gatilho do produto num escritório que não fez nada.
|
|
73
|
+
*/
|
|
74
|
+
export const baselineClaimProblems = (claims, baselineFunctions) => {
|
|
75
|
+
const baseline = new Set(baselineFunctions);
|
|
76
|
+
return claims.flatMap((claim) => (claim.backend.functions ?? [])
|
|
77
|
+
.filter((name) => baseline.has(name))
|
|
78
|
+
.map((name) => `${claim.pluginId} claims function "${name}", which the platform itself deploys`));
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Reivindicar uma function COMUM do escritório — a que mora em `functions/`, ao lado do
|
|
82
|
+
* `microteia.yaml`.
|
|
83
|
+
*
|
|
84
|
+
* É o mesmo defeito de reivindicar uma function da plataforma, com o mesmo par de consequências
|
|
85
|
+
* invisíveis, e por isso é conferido no mesmo lugar. A comum não tem dono por construção: ela é o lugar
|
|
86
|
+
* de quem serve várias extensões, nenhuma pode declará-la (duas reivindicando o mesmo nome é recusa), e
|
|
87
|
+
* o `deploy` a sobe SEMPRE. Reivindicá-la, então, não muda nada enquanto a extensão está instalada — e
|
|
88
|
+
* no dia em que ela sai, a function vira órfã, `--prune` a apaga, e o `deploy` seguinte a repõe. As
|
|
89
|
+
* outras extensões que dependiam dela perdem o backend numa janela que nada explica, e a fonte continua
|
|
90
|
+
* intacta em `functions/`, sem nada na árvore que sequer sugira o que aconteceu.
|
|
91
|
+
*
|
|
92
|
+
* A assimetria com o `pack` é a mesma de `baselineClaimProblems`: o pacote do kernel não sabe o que o
|
|
93
|
+
* escritório vai escrever em `functions/`, então a pergunta só existe do lado de quem instala.
|
|
94
|
+
*/
|
|
95
|
+
export const sharedClaimProblems = (claims, sharedFunctions) => {
|
|
96
|
+
const shared = new Set(sharedFunctions);
|
|
97
|
+
return claims.flatMap((claim) => (claim.backend.functions ?? [])
|
|
98
|
+
.filter((name) => shared.has(name))
|
|
99
|
+
.map((name) => `${claim.pluginId} claims function "${name}", which is a shared function of this `
|
|
100
|
+
+ 'installation'));
|
|
101
|
+
};
|
|
64
102
|
/**
|
|
65
103
|
* Índice reivindicado que não existe na raiz. É conferência de EMPACOTAMENTO, não de provisionamento:
|
|
66
104
|
* a raiz é o que roda em `microteia-dev` e `curatino`, então uma declaração que não esteja lá deixaria
|
package/lib/shared/paths.d.ts
CHANGED
|
@@ -87,4 +87,4 @@ export declare const consoleRoot: string;
|
|
|
87
87
|
* de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
|
|
88
88
|
* política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
|
|
89
89
|
*/
|
|
90
|
-
export declare const CLI_VERSION = "2.
|
|
90
|
+
export declare const CLI_VERSION = "2.18.0";
|
package/lib/shared/paths.js
CHANGED
|
@@ -94,4 +94,4 @@ export const consoleRoot = resolve(here, '..', '..');
|
|
|
94
94
|
* de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
|
|
95
95
|
* política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
|
|
96
96
|
*/
|
|
97
|
-
export const CLI_VERSION = '2.
|
|
97
|
+
export const CLI_VERSION = '2.18.0';
|
|
@@ -16,10 +16,21 @@ export interface HostingConfig {
|
|
|
16
16
|
headers: unknown[];
|
|
17
17
|
}
|
|
18
18
|
export declare const packageFirebaseJson: (hosting: HostingConfig) => string;
|
|
19
|
+
/**
|
|
20
|
+
* O nome do pacote de functions da instalação, e o subcaminho da superfície curada que a Cloud Function
|
|
21
|
+
* do ESCRITÓRIO importa dele: `import { forInstalledExtension } from 'microteia-functions/office'`.
|
|
22
|
+
*
|
|
23
|
+
* O nome é parte do contrato de autoria a partir do momento em que alguém o escreve num import, e por
|
|
24
|
+
* isso ele deixou de ser uma string solta aqui dentro.
|
|
25
|
+
*/
|
|
26
|
+
export declare const FUNCTIONS_PACKAGE_NAME = "microteia-functions";
|
|
27
|
+
export declare const OFFICE_SUBPATH = "office";
|
|
28
|
+
export declare const OFFICE_MODULE = "microteia-functions/office";
|
|
19
29
|
/**
|
|
20
30
|
* As dependências são copiadas INTACTAS do `functions/package.json` do repositório, para que o
|
|
21
31
|
* `package-lock.json` que viaja junto continue válido. O que muda é só o entrypoint (passa a ser o
|
|
22
|
-
* `index.js` que soma kernel e custom)
|
|
32
|
+
* `index.js` que soma kernel e custom), o `build`, que agora compila apenas `custom/`, e o `exports`,
|
|
33
|
+
* que abre a superfície curada para o `custom/` sem custar uma dependência.
|
|
23
34
|
*/
|
|
24
35
|
/**
|
|
25
36
|
* A `version` não é enfeite: sem ela o npm não consegue formar `nome@versão` e todo aviso dele sobre
|