@microteia/console 2.31.1 → 2.35.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 +19 -0
- package/README.md +3 -1
- package/lib/console/exportCatalog.d.ts +39 -0
- package/lib/console/exportCatalog.js +63 -0
- package/lib/kernel/channel.d.ts +2 -1
- package/lib/kernel/channel.js +7 -2
- package/lib/kernel/manifest.js +2 -2
- package/lib/shared/paths.d.ts +1 -1
- package/lib/shared/paths.js +1 -1
- package/lib/vendor/installationDocs.js +9 -4
- package/lib/vendor/pack.js +10 -2
- package/lib/vendor/packageTemplates.js +2 -0
- package/lib/vendor/publish.js +52 -11
- package/package.json +1 -1
package/CONSOLE-VERSIONS.md
CHANGED
|
@@ -22,6 +22,25 @@
|
|
|
22
22
|
- fix(command): what was broken, and what it did to whoever hit it.
|
|
23
23
|
-->
|
|
24
24
|
|
|
25
|
+
#### [2.35.0] — 2026-11-02
|
|
26
|
+
|
|
27
|
+
- feat(publish): the releases repository README links to the official documentation (https://doc.microteia.com), and `publish` rewrites it whenever it differs from the current text — after the catalog, warning instead of failing. It was written only when seeding an empty repository, so the README of an existing one stayed frozen at the text of its first `publish` and no change to it ever arrived. `publish --dry-run` says when it would be rewritten.
|
|
28
|
+
- docs(pack-console): the npm README of the Console links to https://doc.microteia.com.
|
|
29
|
+
|
|
30
|
+
#### [2.34.0] — 2026-11-02
|
|
31
|
+
|
|
32
|
+
- fix(publish): `withEntry` promotes `channels.stable` to the HIGHER of the current and the new version. Publishing out of order — a fix on an older line, `9.180.1` after `9.191.0` — moved `latest` back, and every `unpack --version latest` received the older version. `publish` says whether the version became `latest` or only joined the list.
|
|
33
|
+
- fix(pack): `pack --output <dir>` warns that `publish` reads only `distributions/kernel/<version>/`; the package written elsewhere was simply not found.
|
|
34
|
+
- docs(installation): a platform setting (`SELF_SERVICE_TENANTS`, `AUDIT_ARCHIVE_BUCKET`) reaches the functions with `microteia deploy --only functions`, not `provision`, which only tracks settings declared by extensions and by the installation's `functions/`. The prerequisites and `MICROTEIA.md` §3 said `provision`, which changed nothing.
|
|
35
|
+
|
|
36
|
+
#### [2.33.0] — 2026-10-28
|
|
37
|
+
|
|
38
|
+
- feat(export-catalog): each screen now carries `keys` — every record key of the collection as `name`, `kind`, `fields`, `scope`, `normalize` and `sparse`, plus `ownerPluginId` when an extension declared it — and each field carries `unique: true` when it is set. The conflict `message` and the deprecated `status` stay out: they do not change which reservation a record gets. Without this, the artifact plan proposed as new, on every run, a key the office declared in the Form Design or a third-party extension installed. Always written, even empty; a snapshot taken by an older Console is read as "keys unknown", and the authoring tools say so.
|
|
39
|
+
|
|
40
|
+
#### [2.32.0] — 2026-10-27
|
|
41
|
+
|
|
42
|
+
- feat(export-catalog): each installed extension now carries `fieldPresets` — every `data.field.preset` it declares, as its key and the parts it has (`mask`, `format`, `validate`), never the templates, formatting ops or validation rules. The authoring tools check a field's `mask`, `rules.format.preset` and `rules.validate.check` against it, so a key the dependency does not declare (`cpf-cnpj` for the Brasil extension's `cpfCnpj`) is refused by `check` instead of installing a field that silently has no mask. Always written, even empty; a snapshot taken by an older Console is read as "presets unknown".
|
|
43
|
+
|
|
25
44
|
#### [2.31.1] — 2026-10-26
|
|
26
45
|
|
|
27
46
|
- docs(kernel/manifest): the optional prerequisite `portal-identity-signing` now names a third feature that signs with the functions' runtime account: a portal screen declaring `documentLinks` hands guests V4 signed URLs, and without `iam.serviceAccounts.signBlob` the guest cannot open its documents (the denial shows in the log of `getPortalDocumentUrl`). `microteia grant-signing` grants the same permission for all three.
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# Microteia Console 2.
|
|
1
|
+
# Microteia Console 2.35.0
|
|
2
2
|
|
|
3
3
|
Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
|
|
4
4
|
what it needs are command-line tools that are already on the machine, listed at the bottom.
|
|
5
5
|
|
|
6
|
+
📖 **Documentation: [doc.microteia.com](https://doc.microteia.com)**
|
|
7
|
+
|
|
6
8
|
## Installing it
|
|
7
9
|
|
|
8
10
|
npm i -g @microteia/console
|
|
@@ -55,6 +55,29 @@ export interface CatalogFieldExport {
|
|
|
55
55
|
ownerPluginId?: string;
|
|
56
56
|
aggregation?: Record<string, unknown>;
|
|
57
57
|
compute?: string;
|
|
58
|
+
/** O atalho de chave de um campo só — o kernel o desaçucara em `field:<key>`. */
|
|
59
|
+
unique?: true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Uma chave da coleção (`ScreenDefinition.keys`): o que não pode se repetir. É esquema, como o campo:
|
|
63
|
+
* quem modela precisa saber que ela existe antes de propor outra, e o plano a propunha como nova a
|
|
64
|
+
* cada execução quando ela só existia na instalação.
|
|
65
|
+
*
|
|
66
|
+
* A mensagem do conflito NÃO viaja — é apresentação, e não muda qual reserva um registro recebe. O
|
|
67
|
+
* `status` também não: ele é ignorado pelo kernel desde que a adoção passou a ser estado dele.
|
|
68
|
+
*/
|
|
69
|
+
export interface CatalogKeyExport {
|
|
70
|
+
name: string;
|
|
71
|
+
kind: string;
|
|
72
|
+
fields: string[];
|
|
73
|
+
scope?: string;
|
|
74
|
+
normalize?: {
|
|
75
|
+
trim?: boolean;
|
|
76
|
+
caseInsensitive?: boolean;
|
|
77
|
+
formatPreset?: string;
|
|
78
|
+
};
|
|
79
|
+
sparse?: boolean;
|
|
80
|
+
ownerPluginId?: string;
|
|
58
81
|
}
|
|
59
82
|
export interface CatalogScreenExport {
|
|
60
83
|
key: string;
|
|
@@ -74,6 +97,11 @@ export interface CatalogScreenExport {
|
|
|
74
97
|
parentRelation?: string;
|
|
75
98
|
tagCollection?: string;
|
|
76
99
|
eventPrefix?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Sempre escrita, mesmo vazia, pela mesma razão de `CatalogExtensionExport.exports`: a lista vazia diz
|
|
102
|
+
* "esta tela não tem chave"; a chave ausente é o silêncio de um Console anterior a este campo.
|
|
103
|
+
*/
|
|
104
|
+
keys: CatalogKeyExport[];
|
|
77
105
|
fields: CatalogFieldExport[];
|
|
78
106
|
}
|
|
79
107
|
export interface CatalogListExport {
|
|
@@ -132,6 +160,15 @@ export interface CatalogExtensionDocumentTemplate {
|
|
|
132
160
|
source: 'record' | 'payload';
|
|
133
161
|
parameters: CatalogExtensionDocumentParameter[];
|
|
134
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Um preset de campo que a extensão declara (`data.field.preset`), como as PARTES que ele tem — nunca o
|
|
165
|
+
* template, o formato nem a regra. É contra ele que o `mask` de um campo de outra extensão é conferido:
|
|
166
|
+
* `mask: 'cpf-cnpj'` contra o `cpfCnpj` da Brasil instalava, e o campo ficava sem máscara.
|
|
167
|
+
*/
|
|
168
|
+
export interface CatalogExtensionFieldPreset {
|
|
169
|
+
key: string;
|
|
170
|
+
slots: ('mask' | 'format' | 'validate')[];
|
|
171
|
+
}
|
|
135
172
|
export interface CatalogExtensionExport {
|
|
136
173
|
id: string;
|
|
137
174
|
name: string;
|
|
@@ -146,6 +183,8 @@ export interface CatalogExtensionExport {
|
|
|
146
183
|
navGroups: CatalogExtensionNavGroup[];
|
|
147
184
|
/** Sempre escrita, mesmo vazia, pela mesma razão de `exports`. */
|
|
148
185
|
documentTemplates: CatalogExtensionDocumentTemplate[];
|
|
186
|
+
/** Sempre escrita, mesmo vazia, pela mesma razão de `exports`. */
|
|
187
|
+
fieldPresets: CatalogExtensionFieldPreset[];
|
|
149
188
|
}
|
|
150
189
|
export interface CatalogExport {
|
|
151
190
|
version: number;
|
|
@@ -165,6 +165,33 @@ const documentTemplatesOf = (extension) => {
|
|
|
165
165
|
})
|
|
166
166
|
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
167
167
|
};
|
|
168
|
+
const FIELD_PRESET_POINT = 'data.field.preset';
|
|
169
|
+
/**
|
|
170
|
+
* Os presets de campo de uma cópia instalada. É a mesma leitura que `declaredFieldPresets` faz no
|
|
171
|
+
* kernel — chave e rótulo obrigatórios, e as partes pelo critério do editor de campo —, repetida porque
|
|
172
|
+
* o Console não alcança `src/`; o teste que atravessa a fronteira confere as duas.
|
|
173
|
+
*/
|
|
174
|
+
const fieldPresetsOf = (extension) => {
|
|
175
|
+
const contributes = Array.isArray(extension.manifest?.contributes) ? extension.manifest.contributes : [];
|
|
176
|
+
return contributes
|
|
177
|
+
.map(asRecord)
|
|
178
|
+
.filter((one) => one?.point === FIELD_PRESET_POINT)
|
|
179
|
+
.flatMap((one) => {
|
|
180
|
+
const metadata = asRecord(one.metadata);
|
|
181
|
+
const key = text(metadata?.key);
|
|
182
|
+
if (metadata === undefined || key === undefined || text(metadata.label) === undefined)
|
|
183
|
+
return [];
|
|
184
|
+
return [{
|
|
185
|
+
key,
|
|
186
|
+
slots: [
|
|
187
|
+
...(metadata.mask ? ['mask'] : []),
|
|
188
|
+
...(Array.isArray(metadata.format) && metadata.format.length > 0 ? ['format'] : []),
|
|
189
|
+
...(metadata.validate ? ['validate'] : []),
|
|
190
|
+
],
|
|
191
|
+
}];
|
|
192
|
+
})
|
|
193
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
194
|
+
};
|
|
168
195
|
/**
|
|
169
196
|
* A mesma dedução que a aplicação faz ao ler a tela (`screenDefinitionMapper.ts`): o `origin` gravado
|
|
170
197
|
* quando ele existe, senão `plugin` para quem tem dono e `design` para o que o escritório criou. Ela é
|
|
@@ -200,6 +227,38 @@ const fieldExport = (raw, index, screenOrigin) => {
|
|
|
200
227
|
...(text(field.ownerPluginId) === undefined ? {} : { ownerPluginId: text(field.ownerPluginId) }),
|
|
201
228
|
...(asRecord(field.aggregation) === undefined ? {} : { aggregation: asRecord(field.aggregation) }),
|
|
202
229
|
...(compute === undefined ? {} : { compute }),
|
|
230
|
+
...(field.unique === true ? { unique: true } : {}),
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
const normalizeExport = (raw) => {
|
|
234
|
+
const normalize = asRecord(raw);
|
|
235
|
+
if (normalize === undefined)
|
|
236
|
+
return undefined;
|
|
237
|
+
const formatPreset = text(normalize.formatPreset);
|
|
238
|
+
const picked = {
|
|
239
|
+
...(typeof normalize.trim === 'boolean' ? { trim: normalize.trim } : {}),
|
|
240
|
+
...(typeof normalize.caseInsensitive === 'boolean' ? { caseInsensitive: normalize.caseInsensitive } : {}),
|
|
241
|
+
...(formatPreset === undefined ? {} : { formatPreset }),
|
|
242
|
+
};
|
|
243
|
+
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
244
|
+
};
|
|
245
|
+
/** A chave como o kernel a grava, sem o que é apresentação. Sem nome ou sem campo, não é chave. */
|
|
246
|
+
const keyExport = (raw) => {
|
|
247
|
+
const key = asRecord(raw);
|
|
248
|
+
const name = text(key?.name);
|
|
249
|
+
const kind = text(key?.kind);
|
|
250
|
+
const fields = (Array.isArray(key?.fields) ? key.fields : []).filter((one) => typeof one === 'string' && one !== '');
|
|
251
|
+
if (key === undefined || name === undefined || kind === undefined || fields.length === 0)
|
|
252
|
+
return null;
|
|
253
|
+
const normalize = normalizeExport(key.normalize);
|
|
254
|
+
return {
|
|
255
|
+
name,
|
|
256
|
+
kind,
|
|
257
|
+
fields,
|
|
258
|
+
...(text(key.scope) === undefined ? {} : { scope: text(key.scope) }),
|
|
259
|
+
...(normalize === undefined ? {} : { normalize }),
|
|
260
|
+
...(typeof key.sparse === 'boolean' ? { sparse: key.sparse } : {}),
|
|
261
|
+
...(text(key.ownerPluginId) === undefined ? {} : { ownerPluginId: text(key.ownerPluginId) }),
|
|
203
262
|
};
|
|
204
263
|
};
|
|
205
264
|
const screenExport = (document) => {
|
|
@@ -220,6 +279,9 @@ const screenExport = (document) => {
|
|
|
220
279
|
...(text(data.parentRelation) === undefined ? {} : { parentRelation: text(data.parentRelation) }),
|
|
221
280
|
...(text(data.tagCollection) === undefined ? {} : { tagCollection: text(data.tagCollection) }),
|
|
222
281
|
...(text(data.eventPrefix) === undefined ? {} : { eventPrefix: text(data.eventPrefix) }),
|
|
282
|
+
keys: (Array.isArray(data.keys) ? data.keys : [])
|
|
283
|
+
.map(keyExport)
|
|
284
|
+
.filter((key) => key !== null),
|
|
223
285
|
fields: fields
|
|
224
286
|
.map((field, index) => fieldExport(field, index, origin))
|
|
225
287
|
.filter((field) => field !== null),
|
|
@@ -263,6 +325,7 @@ export const buildCatalogExport = (input) => ({
|
|
|
263
325
|
exports: exportedArtifacts(one),
|
|
264
326
|
navGroups: navGroupsOf(one),
|
|
265
327
|
documentTemplates: documentTemplatesOf(one),
|
|
328
|
+
fieldPresets: fieldPresetsOf(one),
|
|
266
329
|
}))
|
|
267
330
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
268
331
|
screens: input.screens
|
package/lib/kernel/channel.d.ts
CHANGED
|
@@ -63,7 +63,8 @@ export declare const entryOf: (manifest: KernelManifest, urls: {
|
|
|
63
63
|
manifest: string;
|
|
64
64
|
}, publishedAt: string) => ChannelEntry;
|
|
65
65
|
/**
|
|
66
|
-
* Acrescenta a entrada e promove o canal
|
|
66
|
+
* Acrescenta a entrada e promove o canal à MAIOR versão entre a atual e a nova, RECUSANDO uma versão
|
|
67
|
+
* que já está lá.
|
|
67
68
|
*
|
|
68
69
|
* Republicar é a origem de "a mesma versão com dois conteúdos", e ela não tem conserto depois: alguém
|
|
69
70
|
* conferiu um hash contra o download de ontem e a partir dali as duas instalações discordam sobre o que
|
package/lib/kernel/channel.js
CHANGED
|
@@ -109,7 +109,8 @@ export const entryOf = (manifest, urls, publishedAt) => {
|
|
|
109
109
|
};
|
|
110
110
|
const byVersionDescending = (left, right) => compareVersions(right.version, left.version);
|
|
111
111
|
/**
|
|
112
|
-
* Acrescenta a entrada e promove o canal
|
|
112
|
+
* Acrescenta a entrada e promove o canal à MAIOR versão entre a atual e a nova, RECUSANDO uma versão
|
|
113
|
+
* que já está lá.
|
|
113
114
|
*
|
|
114
115
|
* Republicar é a origem de "a mesma versão com dois conteúdos", e ela não tem conserto depois: alguém
|
|
115
116
|
* conferiu um hash contra o download de ontem e a partir dali as duas instalações discordam sobre o que
|
|
@@ -120,9 +121,13 @@ export const withEntry = (index, entry) => {
|
|
|
120
121
|
throw new Error(`${entry.version} is already published, and a released version is never rewritten.\n`
|
|
121
122
|
+ 'Bump the version in package.json and package it again.');
|
|
122
123
|
}
|
|
124
|
+
// Publicar fora de ordem é permitido (uma correção numa linha antiga), e o canal não pode andar para
|
|
125
|
+
// trás por isso: `latest` passaria a entregar uma versão MAIS VELHA a quem atualiza.
|
|
126
|
+
const current = index.channels[STABLE_CHANNEL];
|
|
127
|
+
const stable = current !== undefined && compareVersions(current, entry.version) > 0 ? current : entry.version;
|
|
123
128
|
return {
|
|
124
129
|
schema: INDEX_SCHEMA,
|
|
125
|
-
channels: { ...index.channels, [STABLE_CHANNEL]:
|
|
130
|
+
channels: { ...index.channels, [STABLE_CHANNEL]: stable },
|
|
126
131
|
kernel: [entry, ...index.kernel].sort(byVersionDescending),
|
|
127
132
|
};
|
|
128
133
|
};
|
package/lib/kernel/manifest.js
CHANGED
|
@@ -131,7 +131,7 @@ export const MANUAL_PREREQUISITES = [
|
|
|
131
131
|
+ 'longer. Grant the functions\' runtime service account roles/storage.admin on THAT bucket only (it '
|
|
132
132
|
+ 'reads the bucket\'s retention settings and sets each day\'s retention, which the object roles '
|
|
133
133
|
+ 'do not cover), write AUDIT_ARCHIVE_BUCKET=<bucket> in .env.<projectId> '
|
|
134
|
-
+ 'and run microteia provision. What it protects against, and what it does not: a locked bucket stops '
|
|
134
|
+
+ 'and run microteia deploy --only functions (provision does not carry the platform\'s own settings). What it protects against, and what it does not: a locked bucket stops '
|
|
135
135
|
+ 'deletion and rewriting of what was already archived; the chain makes any change visible on '
|
|
136
136
|
+ 'verification. A project owner can still alter or delete an audit line in Firestore BEFORE its day '
|
|
137
137
|
+ 'is exported (the export runs hourly, about an hour after midnight UTC), turn the archive off (that '
|
|
@@ -147,7 +147,7 @@ export const MANUAL_PREREQUISITES = [
|
|
|
147
147
|
detail: 'By default every new account gets an organization of its own, with that person as the owner. In an '
|
|
148
148
|
+ 'installation shared by several organizations — each one created on purpose — that lets anyone who '
|
|
149
149
|
+ 'can create an account open one. Write SELF_SERVICE_TENANTS=false in .env.<projectId> and run '
|
|
150
|
-
+ 'microteia
|
|
150
|
+
+ 'microteia deploy --only functions: from then on a new account waits to be added to an organization, and only the '
|
|
151
151
|
+ 'installation administrator (SYSTEM) opens organizations, in Access management > Organizations, '
|
|
152
152
|
+ 'naming an owner who already has an account. The first account of the installation gets one '
|
|
153
153
|
+ 'anyway. Any value other than empty, true or false is read as false. Two-step verification is '
|
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.35.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.35.0';
|
|
@@ -191,10 +191,15 @@ deliberately, because you asked.
|
|
|
191
191
|
They live in \`.env.<projectId>\`, next to \`microteia.yaml\`. \`unpack\` creates the file and adds any
|
|
192
192
|
setting a new version introduces, **never overwriting what you filled in**.
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
The value is stamped into each Cloud Function at *its* deploy, so a function already running keeps the
|
|
195
|
+
previous one until it is deployed again. Which command deploys it again depends on who declares the
|
|
196
|
+
setting:
|
|
197
|
+
|
|
198
|
+
- **A setting of an extension, or of your own \`functions/\`** — run \`microteia provision\`. It notices the
|
|
199
|
+
change (by a salted digest in \`microteia.yaml\`) and requeues exactly the functions that carry it.
|
|
200
|
+
- **A setting of the platform itself** (\`SELF_SERVICE_TENANTS\`, \`AUDIT_ARCHIVE_BUCKET\`, …) — run
|
|
201
|
+
\`microteia deploy --only functions\`. \`provision\` does not track them; the deploy compares each
|
|
202
|
+
function's stamp, which includes its environment, and redeploys the ones whose value changed.
|
|
198
203
|
|
|
199
204
|
An empty line means "not configured". A feature that needs it stays off rather than failing halfway.
|
|
200
205
|
|
package/lib/vendor/pack.js
CHANGED
|
@@ -3,7 +3,7 @@ import { basename, join, relative } from 'node:path';
|
|
|
3
3
|
import { flagValue, hasFlag } from '../shared/args.js';
|
|
4
4
|
import { repoRoot } from '../shared/paths.js';
|
|
5
5
|
import { run } from '../shared/run.js';
|
|
6
|
-
import { blank, bullets, heading, info, step, success } from '../shared/ui.js';
|
|
6
|
+
import { blank, bullets, heading, info, step, success, warn } from '../shared/ui.js';
|
|
7
7
|
import { DEFAULT_FUNCTIONS_REGION, KERNEL_COMPONENTS, KERNEL_DISTRIBUTION_ID, MANUAL_PREREQUISITES, MINIMUM_CLI_VERSION, REQUIRED_NODE_VERSION, callableFunctionNames, declaredParametersIn, exportedFunctionNames, parseKernelVersion, parsePreLoginPoints, } from '../kernel/manifest.js';
|
|
8
8
|
import { digestOfFile } from '../shared/digest.js';
|
|
9
9
|
import { kernelTarballName } from '../kernel/channel.js';
|
|
@@ -140,7 +140,8 @@ export const pack = async (args) => {
|
|
|
140
140
|
}
|
|
141
141
|
const baselineFunctions = baselineFunctionsOf(functionNames, claims);
|
|
142
142
|
const baselineIndexes = baselineIndexesOf(rootIndexes.indexes, claims);
|
|
143
|
-
const
|
|
143
|
+
const defaultOutputDir = join(root, 'distributions', 'kernel', version);
|
|
144
|
+
const outputDir = flagValue(args, 'output') ?? defaultOutputDir;
|
|
144
145
|
const stageDir = join(outputDir, 'package');
|
|
145
146
|
const tarball = join(outputDir, kernelTarballName(version));
|
|
146
147
|
heading(`Packaging Microteia kernel ${version} (contract ${kernelVersion})`);
|
|
@@ -264,6 +265,13 @@ export const pack = async (args) => {
|
|
|
264
265
|
`size: ${(digest.bytes / 1_000_000).toFixed(1)} MB, sha256 ${digest.sha256.slice(0, 12)}…`,
|
|
265
266
|
]);
|
|
266
267
|
blank();
|
|
268
|
+
if (outputDir !== defaultOutputDir) {
|
|
269
|
+
// O `publish` lê só `distributions/kernel/<versão>/`: o pacote escrito em outro lugar não é achado,
|
|
270
|
+
// e sem este aviso a recusa dele parecia um pacote que nunca foi feito.
|
|
271
|
+
warn(`--output wrote it outside ${relative(root, defaultOutputDir)}/, and "microteia publish" reads only there.`);
|
|
272
|
+
info(' Copy the folder there to publish it, or install the file directly (below).');
|
|
273
|
+
blank();
|
|
274
|
+
}
|
|
267
275
|
info('Publish it with:');
|
|
268
276
|
bullets(['microteia publish --dry-run (says what would go up)', 'microteia publish']);
|
|
269
277
|
blank();
|
|
@@ -178,6 +178,8 @@ export const consoleReadme = (version) => `# Microteia Console ${version}
|
|
|
178
178
|
Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
|
|
179
179
|
what it needs are command-line tools that are already on the machine, listed at the bottom.
|
|
180
180
|
|
|
181
|
+
📖 **Documentation: [doc.microteia.com](https://doc.microteia.com)**
|
|
182
|
+
|
|
181
183
|
## Installing it
|
|
182
184
|
|
|
183
185
|
npm i -g ${CONSOLE_PACKAGE_NAME}
|
package/lib/vendor/publish.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CLI_VERSION, repoRoot } from '../shared/paths.js';
|
|
|
6
6
|
import { capture, captureOrThrow, run } from '../shared/run.js';
|
|
7
7
|
import { blank, bullets, heading, info, pending, step, success, warn } from '../shared/ui.js';
|
|
8
8
|
import { digestMismatch, digestOfFile } from '../shared/digest.js';
|
|
9
|
-
import { emptyIndex, entryOf, kernelTarballName, parseIndex, publishedBefore, serializeIndex, withEntry, } from '../kernel/channel.js';
|
|
9
|
+
import { emptyIndex, entryOf, kernelTarballName, parseIndex, publishedBefore, serializeIndex, withEntry, STABLE_CHANNEL, } from '../kernel/channel.js';
|
|
10
10
|
import { CONSOLE_CHANGELOG, KERNEL_CHANGELOG, changelogEntries, readChangelog } from './changelogs.js';
|
|
11
11
|
import { CONSOLE_PACKAGE_NAME } from './packageTemplates.js';
|
|
12
12
|
import { assertInsideRepository } from './repository.js';
|
|
@@ -72,16 +72,21 @@ const readPublishedIndex = async (repo) => {
|
|
|
72
72
|
const text = Buffer.from(contents.content, 'base64').toString('utf8');
|
|
73
73
|
return { index: parseIndex(text, `${repo}/${INDEX_PATH}`), sha: contents.sha, empty: false };
|
|
74
74
|
};
|
|
75
|
+
/** O caminho do README DENTRO do repositório de releases. */
|
|
76
|
+
const README_PATH = 'README.md';
|
|
75
77
|
/**
|
|
76
78
|
* O que fica na raiz do repositório de releases, para quem chegar nele sem contexto.
|
|
77
79
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
+
* É documento GERADO, e nenhuma pessoa escreve naquele repositório: por isso é sobreposto sempre que
|
|
81
|
+
* diverge deste texto. Escrito só ao semear, ele congelava no texto do primeiro `publish` — o link da
|
|
82
|
+
* documentação oficial nunca chegaria ao repositório que já existia.
|
|
80
83
|
*/
|
|
81
84
|
const releasesReadme = () => `# Microteia — releases
|
|
82
85
|
|
|
83
86
|
Distribution artifacts for the Microteia platform. **No source lives here**: what travels is compiled.
|
|
84
87
|
|
|
88
|
+
📖 **Documentation: [doc.microteia.com](https://doc.microteia.com)**
|
|
89
|
+
|
|
85
90
|
- \`kernel/index.json\` — the catalog the Microteia Console reads. It lists every published version with
|
|
86
91
|
its size and sha256, and the absolute URLs of the files that carry it.
|
|
87
92
|
- Each version is a release tagged \`kernel-<version>\`, carrying the package and its manifest.
|
|
@@ -94,6 +99,17 @@ Nobody downloads from here by hand:
|
|
|
94
99
|
The Console reads the catalog, verifies what it downloads against the sha256 above, and only then puts
|
|
95
100
|
it in place.
|
|
96
101
|
`;
|
|
102
|
+
const writeReadme = async (repo, sha, message) => {
|
|
103
|
+
const content = Buffer.from(releasesReadme(), 'utf8').toString('base64');
|
|
104
|
+
const args = [
|
|
105
|
+
'api', '--method', 'PUT', `repos/${repo}/contents/${README_PATH}`,
|
|
106
|
+
'-f', `message=${message}`,
|
|
107
|
+
'-f', `content=${content}`,
|
|
108
|
+
];
|
|
109
|
+
if (sha !== null)
|
|
110
|
+
args.push('-f', `sha=${sha}`);
|
|
111
|
+
await captureOrThrow('gh', args);
|
|
112
|
+
};
|
|
97
113
|
/**
|
|
98
114
|
* Dá ao repositório o primeiro commit, porque sem ele não existe release.
|
|
99
115
|
*
|
|
@@ -104,14 +120,20 @@ it in place.
|
|
|
104
120
|
*/
|
|
105
121
|
const seedRepository = async (repo) => {
|
|
106
122
|
pending(`${repo} has no commits yet — writing its README first`);
|
|
107
|
-
|
|
108
|
-
await captureOrThrow('gh', [
|
|
109
|
-
'api', '--method', 'PUT', `repos/${repo}/contents/README.md`,
|
|
110
|
-
'-f', 'message=the releases repository',
|
|
111
|
-
'-f', `content=${content}`,
|
|
112
|
-
]);
|
|
123
|
+
await writeReadme(repo, null, 'the releases repository');
|
|
113
124
|
success(`seeded ${repo}`);
|
|
114
125
|
};
|
|
126
|
+
const staleReadme = async (repo) => {
|
|
127
|
+
const result = await capture('gh', ['api', `repos/${repo}/contents/${README_PATH}`]);
|
|
128
|
+
if (result.code !== 0) {
|
|
129
|
+
if (`${result.stderr}${result.stdout}`.includes('HTTP 404'))
|
|
130
|
+
return { sha: null };
|
|
131
|
+
throw new Error(`Could not read ${README_PATH} from ${repo}.\n${result.stderr.trim() || result.stdout.trim()}`);
|
|
132
|
+
}
|
|
133
|
+
const contents = JSON.parse(result.stdout);
|
|
134
|
+
const text = Buffer.from(contents.content, 'base64').toString('utf8');
|
|
135
|
+
return text === releasesReadme() ? null : { sha: contents.sha };
|
|
136
|
+
};
|
|
115
137
|
const writePublishedIndex = async (repo, index, sha, version) => {
|
|
116
138
|
const content = Buffer.from(serializeIndex(index), 'utf8').toString('base64');
|
|
117
139
|
const args = [
|
|
@@ -178,6 +200,7 @@ const publishKernel = async (root, version, repo, dryRun) => {
|
|
|
178
200
|
const directory = join(root, 'distributions', 'kernel', version);
|
|
179
201
|
const manifestPath = join(directory, 'manifest.json');
|
|
180
202
|
const published = await readPublishedIndex(repo);
|
|
203
|
+
const readme = published.empty ? null : await staleReadme(repo);
|
|
181
204
|
/**
|
|
182
205
|
* O corpo do release cobre o INTERVALO, e não só a versão que está saindo. Um pacote leva tudo o que
|
|
183
206
|
* veio antes dele; um release que só cita a própria entrada esconde as versões que foram empacotadas e
|
|
@@ -190,13 +213,19 @@ const publishKernel = async (root, version, repo, dryRun) => {
|
|
|
190
213
|
manifest: assetUrl(repo, version, 'manifest.json'),
|
|
191
214
|
}, new Date().toISOString()));
|
|
192
215
|
success(`kernel ${version} verified: ${manifest.tarball?.bytes} bytes, sha256 ${manifest.tarball?.sha256.slice(0, 12)}…`);
|
|
193
|
-
|
|
216
|
+
const promotes = next.channels[STABLE_CHANNEL] === version;
|
|
217
|
+
info(promotes
|
|
218
|
+
? ` tag ${tagOf(version)} in ${repo}, and ${INDEX_PATH} promoting it to "latest"`
|
|
219
|
+
: ` tag ${tagOf(version)} in ${repo}, and ${INDEX_PATH} listing it — "latest" stays at `
|
|
220
|
+
+ `${next.channels[STABLE_CHANNEL]}, which is newer`);
|
|
194
221
|
info(notes.versions.length === 1
|
|
195
222
|
? ` release notes: the ${version} entry of ${KERNEL_CHANGELOG}`
|
|
196
223
|
: ` release notes: ${notes.versions.join(', ')} — every entry since ${since ?? 'the first one'}`);
|
|
197
224
|
if (dryRun) {
|
|
198
225
|
if (published.empty)
|
|
199
226
|
info(` ${repo} has no commits, so it would be seeded with a README first`);
|
|
227
|
+
if (readme !== null)
|
|
228
|
+
info(` ${README_PATH} in ${repo} differs from the current text, so it would be rewritten`);
|
|
200
229
|
info(` the catalog would go from ${published.index.kernel.length} to ${next.kernel.length} version(s)`);
|
|
201
230
|
return;
|
|
202
231
|
}
|
|
@@ -231,7 +260,19 @@ const publishKernel = async (root, version, repo, dryRun) => {
|
|
|
231
260
|
success(`assets published for ${version}`);
|
|
232
261
|
// O catálogo por último, sempre. Ver a nota de ordem no topo deste módulo.
|
|
233
262
|
await writePublishedIndex(repo, next, published.sha, version);
|
|
234
|
-
success(
|
|
263
|
+
success(promotes
|
|
264
|
+
? `${INDEX_PATH} updated — "microteia unpack --version latest" now takes ${version}`
|
|
265
|
+
: `${INDEX_PATH} updated — "microteia unpack --version ${version}" takes it; "latest" stays at `
|
|
266
|
+
+ `${next.channels[STABLE_CHANNEL]}`);
|
|
267
|
+
/**
|
|
268
|
+
* Depois do catálogo, e sem derrubar a publicação: o README não é o que alguém instala, e uma
|
|
269
|
+
* falha nele não pode desfazer — nem fazer parecer desfeito — um release que já está no ar.
|
|
270
|
+
*/
|
|
271
|
+
if (readme !== null) {
|
|
272
|
+
await writeReadme(repo, readme.sha, `README for kernel ${version}`)
|
|
273
|
+
.then(() => success(`${README_PATH} in ${repo} rewritten`))
|
|
274
|
+
.catch((error) => warn(`${README_PATH} in ${repo} could not be rewritten: ${error instanceof Error ? error.message : String(error)}`));
|
|
275
|
+
}
|
|
235
276
|
}
|
|
236
277
|
finally {
|
|
237
278
|
await rm(notesRoot, { recursive: true, force: true });
|