@microteia/console 2.28.0 → 2.28.1
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 +5 -0
- package/README.md +1 -1
- package/lib/console/deploy.js +2 -1
- package/lib/console/deployFunctions.d.ts +18 -0
- package/lib/console/deployFunctions.js +34 -3
- package/lib/console/provision.js +2 -2
- package/lib/console/regionOrphans.d.ts +9 -4
- package/lib/console/regionOrphans.js +13 -8
- package/lib/shared/paths.d.ts +1 -1
- package/lib/shared/paths.js +1 -1
- package/package.json +1 -1
package/CONSOLE-VERSIONS.md
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
- fix(command): what was broken, and what it did to whoever hit it.
|
|
23
23
|
-->
|
|
24
24
|
|
|
25
|
+
#### [2.28.1] — 2026-10-14
|
|
26
|
+
|
|
27
|
+
- fix(console/deploy): a function whose source declares `retry: true` is no longer refused on its first deploy. Without a terminal the Firebase CLI does not ask "Would you like to proceed with deployment?" — it aborts with "Pass the --force option to deploy functions with a failure policy", so `runAggregateBackfill` and `runRecordKeyAdoption` never went up through `microteia deploy` or `provision`. That one function is now redeployed with `--force`, and only when the project is known to hold no copy of it in another region: the same flag would delete that copy without asking, which stays a `--prune` decision. When a copy stands or the project could not be listed, the refusal is terminal and the hint points to `microteia deploy --prune`.
|
|
28
|
+
- refactor(console/regionOrphans): `clearRegionOrphans` returns the names whose old-region copy is still standing (`null` when the project could not be listed) instead of a boolean.
|
|
29
|
+
|
|
25
30
|
#### [2.28.0] — 2026-10-13
|
|
26
31
|
|
|
27
32
|
- feat(examples): the Console ships `business-days` as authoring source — a collection the kernel reads without knowing its name (`time.business.calendar`), and a code view that edits data no field type describes.
|
package/README.md
CHANGED
package/lib/console/deploy.js
CHANGED
|
@@ -185,7 +185,7 @@ export const deploy = async (args) => {
|
|
|
185
185
|
// ANTES de deployar: uma cópia da mesma function numa região que o pacote abandonou faz o
|
|
186
186
|
// `firebase` recusar o deploy dela, tentativa após tentativa, e faz um gatilho de Firestore
|
|
187
187
|
// disparar duas vezes por gravação enquanto ninguém percebe.
|
|
188
|
-
await clearRegionOrphans(projectId, own, region, hasFlag(args, 'prune'), deployedNow);
|
|
188
|
+
const standingCopies = await clearRegionOrphans(projectId, own, region, hasFlag(args, 'prune'), deployedNow);
|
|
189
189
|
// Numa instalação nova não há carimbo nenhum com que comparar, e o mecanismo cai sozinho no
|
|
190
190
|
// caminho de sempre — não é preciso um desvio para isso. `--all` é o pedido explícito de subir
|
|
191
191
|
// tudo, para a configuração trocada por fora, que carimbo nenhum enxerga.
|
|
@@ -194,6 +194,7 @@ export const deploy = async (args) => {
|
|
|
194
194
|
cwd: install,
|
|
195
195
|
delaySeconds,
|
|
196
196
|
force: firstInstall,
|
|
197
|
+
standingCopies,
|
|
197
198
|
}, { deployed: deployedNow, region, all: hasFlag(args, 'all') });
|
|
198
199
|
refreshedStamps = report.refreshed;
|
|
199
200
|
skippedFunctions = report.skipped.length;
|
|
@@ -4,6 +4,11 @@ export interface FunctionDeployOptions {
|
|
|
4
4
|
delaySeconds: number;
|
|
5
5
|
/** `--force` deixa o Firebase configurar sozinho a política de limpeza do Artifact Registry. */
|
|
6
6
|
force: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* As functions que ainda têm cópia numa região abandonada, como `clearRegionOrphans` devolve;
|
|
9
|
+
* `null` quando não deu para saber. Só fora dele a recusa da política de repetição é contornada.
|
|
10
|
+
*/
|
|
11
|
+
standingCopies: ReadonlySet<string> | null;
|
|
7
12
|
}
|
|
8
13
|
export interface FunctionDeployFailure {
|
|
9
14
|
name: string;
|
|
@@ -39,7 +44,20 @@ export interface TerminalFailure {
|
|
|
39
44
|
match: RegExp;
|
|
40
45
|
hint: string[];
|
|
41
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A recusa do `firebase` sem terminal diante de uma function que passa a ter `retry`.
|
|
49
|
+
*
|
|
50
|
+
* Com terminal ele pergunta "Would you like to proceed with deployment?"; sem, recusa e pede
|
|
51
|
+
* `--force`. A pergunta é sobre o que o PRÓPRIO código declarou (`retry: true` no gatilho), e quem
|
|
52
|
+
* declarou já respondeu: repetir a entrega é o que torna o gatilho confiável. Por isso a recusa é
|
|
53
|
+
* contornada com `--force` naquela function, e só nela — e só quando não há cópia dela em outra
|
|
54
|
+
* região, porque o mesmo `--force` faria o `firebase` apagar essa cópia sem perguntar.
|
|
55
|
+
*
|
|
56
|
+
* A pergunta só existe na primeira vez: com a function no ar já com `retry`, ela não volta.
|
|
57
|
+
*/
|
|
58
|
+
export declare const FAILURE_POLICY_REFUSAL: RegExp;
|
|
42
59
|
export declare const terminalFailure: (output: string) => TerminalFailure | undefined;
|
|
60
|
+
export declare const mayConfirmRetryPolicy: (name: string, output: string, options: FunctionDeployOptions) => boolean;
|
|
43
61
|
export declare const deployFunctionsOneByOne: (names: string[], options: FunctionDeployOptions) => Promise<FunctionDeployReport>;
|
|
44
62
|
export declare const reportFailures: (failures: FunctionDeployFailure[]) => void;
|
|
45
63
|
export declare const reportNotices: (notices: string[]) => void;
|
|
@@ -64,6 +64,18 @@ export const failureReason = (output) => {
|
|
|
64
64
|
?? 'no output from firebase';
|
|
65
65
|
return chosen.length > REASON_LIMIT ? `${chosen.slice(0, REASON_LIMIT - 1)}…` : chosen;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* A recusa do `firebase` sem terminal diante de uma function que passa a ter `retry`.
|
|
69
|
+
*
|
|
70
|
+
* Com terminal ele pergunta "Would you like to proceed with deployment?"; sem, recusa e pede
|
|
71
|
+
* `--force`. A pergunta é sobre o que o PRÓPRIO código declarou (`retry: true` no gatilho), e quem
|
|
72
|
+
* declarou já respondeu: repetir a entrega é o que torna o gatilho confiável. Por isso a recusa é
|
|
73
|
+
* contornada com `--force` naquela function, e só nela — e só quando não há cópia dela em outra
|
|
74
|
+
* região, porque o mesmo `--force` faria o `firebase` apagar essa cópia sem perguntar.
|
|
75
|
+
*
|
|
76
|
+
* A pergunta só existe na primeira vez: com a function no ar já com `retry`, ela não volta.
|
|
77
|
+
*/
|
|
78
|
+
export const FAILURE_POLICY_REFUSAL = /Pass the --force option to deploy functions with a failure policy/;
|
|
67
79
|
const TERMINAL_FAILURES = [
|
|
68
80
|
{
|
|
69
81
|
match: /Aborting because deletion cannot proceed in non-interactive mode/,
|
|
@@ -74,6 +86,15 @@ const TERMINAL_FAILURES = [
|
|
|
74
86
|
' microteia deploy --prune',
|
|
75
87
|
],
|
|
76
88
|
},
|
|
89
|
+
{
|
|
90
|
+
match: FAILURE_POLICY_REFUSAL,
|
|
91
|
+
hint: [
|
|
92
|
+
'The function now retries failed deliveries, and the Firebase CLI asks to confirm that once.',
|
|
93
|
+
'It was not confirmed here because the project may still hold a copy of the function in another',
|
|
94
|
+
'region, which the same confirmation would delete without asking. Remove old copies first:',
|
|
95
|
+
' microteia deploy --prune',
|
|
96
|
+
],
|
|
97
|
+
},
|
|
77
98
|
{
|
|
78
99
|
match: /does not have a default and cannot be answered in non-interactive mode/,
|
|
79
100
|
hint: [
|
|
@@ -97,9 +118,9 @@ export const terminalFailure = (output) => {
|
|
|
97
118
|
* gatilho falha enquanto as callables sobem normalmente. Repetir resolve, e sem isso a primeira
|
|
98
119
|
* instalação parece quebrada quando está apenas incompleta.
|
|
99
120
|
*/
|
|
100
|
-
const deployOne = async (name, options) => {
|
|
121
|
+
const deployOne = async (name, options, force = options.force) => {
|
|
101
122
|
const args = ['deploy', '--only', `functions:${name}`, '--project', options.projectId];
|
|
102
|
-
if (
|
|
123
|
+
if (force)
|
|
103
124
|
args.push('--force');
|
|
104
125
|
const startedAt = Date.now();
|
|
105
126
|
const result = await capture(FIREBASE_BIN, args, { cwd: options.cwd });
|
|
@@ -111,6 +132,10 @@ const deployOne = async (name, options) => {
|
|
|
111
132
|
seconds: Math.round((Date.now() - startedAt) / 1000),
|
|
112
133
|
};
|
|
113
134
|
};
|
|
135
|
+
export const mayConfirmRetryPolicy = (name, output, options) => !options.force
|
|
136
|
+
&& options.standingCopies !== null
|
|
137
|
+
&& !options.standingCopies.has(name)
|
|
138
|
+
&& FAILURE_POLICY_REFUSAL.test(stripVTControlCharacters(output));
|
|
114
139
|
export const deployFunctionsOneByOne = async (names, options) => {
|
|
115
140
|
const deployed = [];
|
|
116
141
|
const failed = [];
|
|
@@ -134,9 +159,15 @@ export const deployFunctionsOneByOne = async (names, options) => {
|
|
|
134
159
|
const name = remaining[index];
|
|
135
160
|
const label = `[${index + 1}/${remaining.length}] ${name}`;
|
|
136
161
|
pending(label);
|
|
137
|
-
|
|
162
|
+
let { ok, output, notices: given, seconds } = await deployOne(name, options);
|
|
138
163
|
for (const notice of given)
|
|
139
164
|
notices.add(notice);
|
|
165
|
+
if (!ok && mayConfirmRetryPolicy(name, output, options)) {
|
|
166
|
+
info(` ${name} now retries failed deliveries, as its source declares: confirming it`);
|
|
167
|
+
({ ok, output, notices: given, seconds } = await deployOne(name, options, true));
|
|
168
|
+
for (const notice of given)
|
|
169
|
+
notices.add(notice);
|
|
170
|
+
}
|
|
140
171
|
if (ok) {
|
|
141
172
|
success(`${label} (${seconds}s)`);
|
|
142
173
|
deployed.push(name);
|
package/lib/console/provision.js
CHANGED
|
@@ -428,8 +428,8 @@ export const provisionBackend = async (options) => {
|
|
|
428
428
|
}
|
|
429
429
|
// A extensão que mudou de região deixa a cópia antiga para trás igual ao kernel, e ali o efeito
|
|
430
430
|
// é o mesmo: deploy recusado em toda tentativa, e gatilho disparando em duas regiões.
|
|
431
|
-
await clearRegionOrphans(projectId, toDeploy, region, options.prune, await projectFunctions());
|
|
432
|
-
const deployOptions = { projectId, cwd: install, delaySeconds, force: false };
|
|
431
|
+
const standingCopies = await clearRegionOrphans(projectId, toDeploy, region, options.prune, await projectFunctions());
|
|
432
|
+
const deployOptions = { projectId, cwd: install, delaySeconds, force: false, standingCopies };
|
|
433
433
|
if (!probing)
|
|
434
434
|
info(` deploying ${toDeploy.length} function(s), one at a time…`);
|
|
435
435
|
const report = probing
|
|
@@ -50,10 +50,15 @@ export declare const deleteCommand: (projectId: string, orphan: RegionOrphan) =>
|
|
|
50
50
|
* Avisa por padrão e só apaga com `--prune`, como `settleOrphans` faz com a function que perdeu dono.
|
|
51
51
|
* Apagar uma Cloud Function é irreversível e é do escritório, não nosso: o comando tem de ser pedido.
|
|
52
52
|
*/
|
|
53
|
-
export declare const settleRegionOrphans: (projectId: string, orphans: RegionOrphan[], prune: boolean) => Promise<
|
|
53
|
+
export declare const settleRegionOrphans: (projectId: string, orphans: RegionOrphan[], prune: boolean) => Promise<ReadonlySet<string>>;
|
|
54
54
|
/**
|
|
55
|
-
* O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve
|
|
56
|
-
*
|
|
55
|
+
* O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve os nomes cuja cópia antiga
|
|
56
|
+
* CONTINUA de pé, ou `null` quando não deu para verificar — e não bloqueia o deploy em nenhum dos dois
|
|
57
|
+
* casos, pela mesma razão de `listDeployedFunctions`.
|
|
58
|
+
*
|
|
59
|
+
* O conjunto é o que autoriza o `--force` pontual de `deployFunctionsOneByOne`: com ele o `firebase`
|
|
60
|
+
* aceita a política de repetição E apaga sem perguntar a cópia de outra região, e essa segunda decisão
|
|
61
|
+
* é do escritório, pedida com `--prune`. Não saber equivale a ter cópia.
|
|
57
62
|
*/
|
|
58
63
|
export declare const clearRegionOrphans: (projectId: string, wanted: string[], region: string, prune: boolean,
|
|
59
64
|
/**
|
|
@@ -61,4 +66,4 @@ export declare const clearRegionOrphans: (projectId: string, wanted: string[], r
|
|
|
61
66
|
* a mesma coisa ao mesmo projeto não é caro, é ambíguo: as duas respostas podem divergir, e a
|
|
62
67
|
* decisão passa a depender de qual delas o código olhou.
|
|
63
68
|
*/
|
|
64
|
-
listed?: DeployedFunction[] | null) => Promise<
|
|
69
|
+
listed?: DeployedFunction[] | null) => Promise<ReadonlySet<string> | null>;
|
|
@@ -51,7 +51,7 @@ export const deleteCommand = (projectId, orphan) => `firebase functions:delete $
|
|
|
51
51
|
*/
|
|
52
52
|
export const settleRegionOrphans = async (projectId, orphans, prune) => {
|
|
53
53
|
if (orphans.length === 0)
|
|
54
|
-
return
|
|
54
|
+
return new Set();
|
|
55
55
|
blank();
|
|
56
56
|
warn(`${orphans.length} function(s) are still deployed in a region this package no longer uses:`);
|
|
57
57
|
bullets(orphans.map((orphan) => `${orphan.name} (${orphan.region})`));
|
|
@@ -66,11 +66,11 @@ export const settleRegionOrphans = async (projectId, orphans, prune) => {
|
|
|
66
66
|
info(' or one at a time:');
|
|
67
67
|
bullets(orphans.map((orphan) => deleteCommand(projectId, orphan)));
|
|
68
68
|
blank();
|
|
69
|
-
return
|
|
69
|
+
return new Set(orphans.map((orphan) => orphan.name));
|
|
70
70
|
}
|
|
71
71
|
const plural = orphans.length === 1 ? 'copy' : 'copies';
|
|
72
72
|
info(` --prune: removing the ${orphans.length} old ${plural} before deploying.`);
|
|
73
|
-
|
|
73
|
+
const standing = new Set();
|
|
74
74
|
for (const orphan of orphans) {
|
|
75
75
|
const label = `${orphan.name} (${orphan.region})`;
|
|
76
76
|
pending(label);
|
|
@@ -87,15 +87,20 @@ export const settleRegionOrphans = async (projectId, orphans, prune) => {
|
|
|
87
87
|
failure(`${label} could not be removed`);
|
|
88
88
|
info(` ${failureReason(`${result.stdout}${result.stderr}`)}`);
|
|
89
89
|
info(` ${deleteCommand(projectId, orphan)}`);
|
|
90
|
-
|
|
90
|
+
standing.add(orphan.name);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
blank();
|
|
94
|
-
return
|
|
94
|
+
return standing;
|
|
95
95
|
};
|
|
96
96
|
/**
|
|
97
|
-
* O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve
|
|
98
|
-
*
|
|
97
|
+
* O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve os nomes cuja cópia antiga
|
|
98
|
+
* CONTINUA de pé, ou `null` quando não deu para verificar — e não bloqueia o deploy em nenhum dos dois
|
|
99
|
+
* casos, pela mesma razão de `listDeployedFunctions`.
|
|
100
|
+
*
|
|
101
|
+
* O conjunto é o que autoriza o `--force` pontual de `deployFunctionsOneByOne`: com ele o `firebase`
|
|
102
|
+
* aceita a política de repetição E apaga sem perguntar a cópia de outra região, e essa segunda decisão
|
|
103
|
+
* é do escritório, pedida com `--prune`. Não saber equivale a ter cópia.
|
|
99
104
|
*/
|
|
100
105
|
export const clearRegionOrphans = async (projectId, wanted, region, prune,
|
|
101
106
|
/**
|
|
@@ -106,6 +111,6 @@ export const clearRegionOrphans = async (projectId, wanted, region, prune,
|
|
|
106
111
|
listed) => {
|
|
107
112
|
const deployed = listed === undefined ? await listDeployedFunctions(projectId) : listed;
|
|
108
113
|
if (deployed === null)
|
|
109
|
-
return
|
|
114
|
+
return null;
|
|
110
115
|
return settleRegionOrphans(projectId, regionOrphans(deployed, wanted, region), prune);
|
|
111
116
|
};
|
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.28.
|
|
90
|
+
export declare const CLI_VERSION = "2.28.1";
|
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.28.
|
|
97
|
+
export const CLI_VERSION = '2.28.1';
|