@omega.js/desktop 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/runner.js +134 -37
- package/dist/defaults/.github/workflows/build.yml +2 -0
- package/dist/runner/job-started.js +1 -1
- package/dist/test/suites/build/runner.test.js +610 -3
- package/dist/vendor/config/index.js +5 -1
- package/dist/vendor/config/repo.js +24 -0
- package/dist/vendor/devkit/brand-version.js +162 -1
- package/dist/vendor/devkit/deploy.js +64 -4
- package/dist/vendor/devkit/git-remote.js +78 -1
- package/dist/vendor/devkit/local.js +51 -3
- package/dist/vendor/devkit/lockfile.js +127 -0
- package/dist/vendor/devkit/pack-local.js +4 -7
- package/dist/vendor/devkit/preludes/origin-heal.js +35 -49
- package/dist/vendor/devkit/target-secrets.js +34 -45
- package/docs/index.md +1 -1
- package/docs/releasing.md +4 -2
- package/docs/runner.md +12 -10
- package/docs/shared/brands.md +1 -1
- package/docs/shared/config.md +7 -0
- package/docs/shared/deploys.md +30 -7
- package/docs/shared/local-dev.md +5 -3
- package/docs/signing.md +1 -1
- package/package.json +2 -2
|
@@ -43,7 +43,7 @@ const { resolveHook, loadHook } = require('./hooks.js');
|
|
|
43
43
|
const { toSiteGlobal } = require('./site-global.js');
|
|
44
44
|
const { PLATFORMS, FORMATS, enabledFormats, formatKeys, desktopProductName, sanitizeProductName, desktopArtifactName, desktopArtifactNames } = require('./platforms.js');
|
|
45
45
|
const { resolveWinbackOffer, WINBACK_OFFER_DEFAULTS, WINBACK_DURATIONS } = require('./winback.js');
|
|
46
|
-
const { REPO_PROVIDERS, HOSTING_PROVIDERS, repoBlock, sourceRepo, releasesRepo, websiteRepo, hostingProvider, pagesHost, brandVisibility } = require('./repo.js');
|
|
46
|
+
const { REPO_PROVIDERS, HOSTING_PROVIDERS, repoBlock, sourceRepo, repoDrift, releasesRepo, websiteRepo, hostingProvider, pagesHost, brandVisibility } = require('./repo.js');
|
|
47
47
|
const { isDemoProject } = require('./demo.js');
|
|
48
48
|
const { deriveBundleIdPrefix, composeBundleId } = require('./bundle-id.js');
|
|
49
49
|
const { DEV_FACT_CHANNEL, devFactMissing } = require('./dev-facts.js');
|
|
@@ -222,6 +222,10 @@ module.exports = {
|
|
|
222
222
|
// The SOURCE monorepo `<brand.id>-omega`: the dispatch, the repo secrets, the
|
|
223
223
|
// scaffolded workflows and the CMS commits all address this one
|
|
224
224
|
sourceRepo,
|
|
225
|
+
// Whether the repo `origin` names IS that source repo (#934): the one
|
|
226
|
+
// comparison and the one drift line, stated at boot and refused on by the
|
|
227
|
+
// verbs that act on the derived repo
|
|
228
|
+
repoDrift,
|
|
225
229
|
// The brand's ONE public releases repo `<brand.id>-releases`, always public:
|
|
226
230
|
// the one home every release reader takes its address from
|
|
227
231
|
releasesRepo,
|
|
@@ -108,6 +108,29 @@ function sourceRepo(config) {
|
|
|
108
108
|
return derivedRepo(config, 'omega');
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Whether the repo a brand's `origin` names IS the source repo its config
|
|
113
|
+
* derives ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)): the ONE
|
|
114
|
+
* comparison, and the ONE wording of its answer, for every reader that holds
|
|
115
|
+
* the two side by side. The boot prelude states it in one line; the manage walk
|
|
116
|
+
* and the deploy, which act ON the derived repo, refuse with it.
|
|
117
|
+
*
|
|
118
|
+
* The WHOLE slug is compared, case-insensitively (GitHub's own comparison), so
|
|
119
|
+
* a rename drifts as surely as a transfer: an owner-only compare let a renamed
|
|
120
|
+
* repo through while every reader kept deriving the old name.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} originSlug - The origin's `Owner/name`.
|
|
123
|
+
* @param {object} config - Composed omega config.
|
|
124
|
+
* @returns {string|null} The drift line, or null when the two agree or the
|
|
125
|
+
* config derives no source repo (no org, or no brand id: nothing to compare).
|
|
126
|
+
*/
|
|
127
|
+
function repoDrift(originSlug, config) {
|
|
128
|
+
const derived = sourceRepo(config);
|
|
129
|
+
if (!derived || derived.slug.toLowerCase() === originSlug.toLowerCase()) return null;
|
|
130
|
+
|
|
131
|
+
return `origin is ${originSlug} but config derives ${derived.slug}: fix repo.org in config/omega.json5 or move the repo`;
|
|
132
|
+
}
|
|
133
|
+
|
|
111
134
|
/**
|
|
112
135
|
* The brand's ONE public releases repo: `<brand.id>-releases` under the same
|
|
113
136
|
* org, ALWAYS public, because the desktop updater polls it with no token
|
|
@@ -256,6 +279,7 @@ module.exports = {
|
|
|
256
279
|
HOSTING_PROVIDERS,
|
|
257
280
|
repoBlock,
|
|
258
281
|
sourceRepo,
|
|
282
|
+
repoDrift,
|
|
259
283
|
releasesRepo,
|
|
260
284
|
websiteRepo,
|
|
261
285
|
hostingProvider,
|
|
@@ -21,12 +21,21 @@
|
|
|
21
21
|
* mixed @omega.js FAMILY. Same verdict, different subject: that one is about
|
|
22
22
|
* the framework packages a brand installed, this one about the brand's own
|
|
23
23
|
* release number.
|
|
24
|
+
*
|
|
25
|
+
* Beside it, the LOCKFILE gate
|
|
26
|
+
* ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)): the registry
|
|
27
|
+
* deploy lane ships the brand's own `package-lock.json`, and the runner's
|
|
28
|
+
* `npm ci` installs exactly what it says, so `assertBrandLockfile` refuses a
|
|
29
|
+
* lock that disagrees with the manifests' @omega.js specs before anything is
|
|
30
|
+
* pushed. `brandLockfileDrift` is the read both the gate and the lock
|
|
31
|
+
* regeneration (`lockfile.js`) share.
|
|
24
32
|
*/
|
|
25
33
|
const path = require('path');
|
|
26
34
|
const jetpack = require('fs-jetpack');
|
|
27
35
|
|
|
28
36
|
const { resolveBrandRoot } = require('../config/index.js');
|
|
29
37
|
const Logger = require('./logger.js');
|
|
38
|
+
const { isRegistrySpec, resolveWanted } = require('./update.js');
|
|
30
39
|
|
|
31
40
|
const defaultLogger = new Logger('brand-version');
|
|
32
41
|
|
|
@@ -261,4 +270,156 @@ function assertBrandVersion({ dir }) {
|
|
|
261
270
|
return { brandRoot, name, version };
|
|
262
271
|
}
|
|
263
272
|
|
|
264
|
-
|
|
273
|
+
// The family's scope: the only dependencies the lockfile gate reads.
|
|
274
|
+
const SCOPE = '@omega.js/';
|
|
275
|
+
|
|
276
|
+
// A registry entry's `resolved` is a URL; a link target, a packed tarball or a
|
|
277
|
+
// directory is a path, which a runner has nothing at.
|
|
278
|
+
const REGISTRY_RESOLVED = /^https?:\/\//;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The lock entry npm would use for `name` as declared by the manifest at
|
|
282
|
+
* `where`: its own nested copy first, then the hoisted one at the root, the
|
|
283
|
+
* order node resolves in.
|
|
284
|
+
*
|
|
285
|
+
* @param {object} packages - The lock's `packages` map.
|
|
286
|
+
* @param {string} where - The manifest's dir relative to the root ('' for the root).
|
|
287
|
+
* @param {string} name - The dependency name.
|
|
288
|
+
* @returns {string|null} The lock key, or null when neither slot exists.
|
|
289
|
+
*/
|
|
290
|
+
function lockKeyFor(packages, where, name) {
|
|
291
|
+
const keys = where ? [`${where}/node_modules/${name}`, `node_modules/${name}`] : [`node_modules/${name}`];
|
|
292
|
+
return keys.find((key) => packages[key]) || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Why a lock entry cannot satisfy a registry spec on a runner, or null when it
|
|
297
|
+
* does.
|
|
298
|
+
*
|
|
299
|
+
* @param {object|undefined} entry - The lock entry.
|
|
300
|
+
* @param {string} spec - The manifest's registry spec.
|
|
301
|
+
* @returns {string|null} The reason, worded to follow `<name> <spec> `.
|
|
302
|
+
*/
|
|
303
|
+
function entryDrift(entry, spec) {
|
|
304
|
+
if (!entry) return 'is not in the lock';
|
|
305
|
+
if (entry.link) return `is locked as a link to ${entry.resolved}`;
|
|
306
|
+
if (entry.resolved && !REGISTRY_RESOLVED.test(entry.resolved)) return `is locked at the path ${entry.resolved}`;
|
|
307
|
+
if (resolveWanted(spec, [entry.version]) !== entry.version) return `is locked at ${entry.version || 'no version'}`;
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The lock's STALE path entries: a key that names a folder under the root (a
|
|
313
|
+
* workspace, or a link target) rather than a `node_modules/` slot, where that
|
|
314
|
+
* folder is not on disk. A renamed target leaves one behind (`targets/website`
|
|
315
|
+
* after the move to `targets/web`), and npm keeps honoring the dependencies it
|
|
316
|
+
* declares on every lock-only run.
|
|
317
|
+
*
|
|
318
|
+
* @param {object} options
|
|
319
|
+
* @param {string} options.root - The install root the lock's keys are relative to.
|
|
320
|
+
* @param {object} options.packages - The lock's `packages` map.
|
|
321
|
+
* @returns {string[]} The stale keys, nested ones included.
|
|
322
|
+
*/
|
|
323
|
+
function staleLockPaths({ root, packages }) {
|
|
324
|
+
return Object.keys(packages).filter((key) => key !== ''
|
|
325
|
+
&& !key.split('/').includes('node_modules')
|
|
326
|
+
&& !jetpack.exists(path.join(root, key)));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Every @omega.js registry spec in the brand's manifests (the root and each
|
|
331
|
+
* `targets/<name>/package.json`) that the brand's OWN lockfile does not
|
|
332
|
+
* satisfy. Read-only. `file:`/`link:`/git specs are the linked lane's, which
|
|
333
|
+
* regenerates its lock in the pack step, so only registry specs are read. A
|
|
334
|
+
* STALE lock entry (`staleLockPaths`) declaring an @omega.js spec that is NOT a
|
|
335
|
+
* registry one is drift too: npm would re-create the link it names on the next
|
|
336
|
+
* lock-only run.
|
|
337
|
+
*
|
|
338
|
+
* @param {object} options
|
|
339
|
+
* @param {string} options.root - The install root (a brand root, or a standalone target).
|
|
340
|
+
* @returns {{ lockPath: string, lock: object|null, drift: Array<{ manifest: string, name: string, spec: string, key: string|null, entry: object|undefined, reason: string }> }}
|
|
341
|
+
* `lock` is null when the root has none; `manifest` is the declaring
|
|
342
|
+
* manifest's dir relative to the root, `package.json` for the root itself.
|
|
343
|
+
*/
|
|
344
|
+
function brandLockfileDrift({ root }) {
|
|
345
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
346
|
+
const lock = jetpack.exists(lockPath) === 'file' ? jetpack.read(lockPath, 'json') : null;
|
|
347
|
+
const packages = (lock && lock.packages) || {};
|
|
348
|
+
|
|
349
|
+
const manifests = [
|
|
350
|
+
{ where: '', file: path.join(root, 'package.json') },
|
|
351
|
+
...listTargetPackages({ brandRoot: root }).map((target) => ({ where: `targets/${target.name}`, file: target.manifest })),
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
const drift = [];
|
|
355
|
+
|
|
356
|
+
for (const { where, file } of manifests) {
|
|
357
|
+
const pkg = jetpack.read(file, 'json') || {};
|
|
358
|
+
const declared = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
359
|
+
|
|
360
|
+
for (const [name, spec] of Object.entries(declared)) {
|
|
361
|
+
if (!name.startsWith(SCOPE) || !isRegistrySpec(spec)) continue;
|
|
362
|
+
|
|
363
|
+
const key = lockKeyFor(packages, where, name);
|
|
364
|
+
const entry = key ? packages[key] : undefined;
|
|
365
|
+
const reason = entryDrift(entry, spec);
|
|
366
|
+
|
|
367
|
+
if (reason) drift.push({ manifest: where || 'package.json', name, spec, key, entry, reason });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const stale of staleLockPaths({ root, packages })) {
|
|
372
|
+
const declared = { ...(packages[stale].dependencies || {}), ...(packages[stale].devDependencies || {}) };
|
|
373
|
+
|
|
374
|
+
for (const [name, spec] of Object.entries(declared)) {
|
|
375
|
+
if (!name.startsWith(SCOPE) || isRegistrySpec(spec)) continue;
|
|
376
|
+
|
|
377
|
+
// No key: nothing here is the dependency's own entry, and the pruning
|
|
378
|
+
// of stale paths (lockfile.js) is what removes this one.
|
|
379
|
+
drift.push({ manifest: stale, name, spec, key: null, entry: undefined, reason: `is declared by a stale lock entry (${stale} is not on disk)` });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { lockPath, lock, drift };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The deploy gate on the registry lane: the brand's lockfile must exist and
|
|
388
|
+
* carry every @omega.js registry spec of its manifests as a registry entry at
|
|
389
|
+
* a version that satisfies it. Read-only, so a dry run runs it too. Agreement
|
|
390
|
+
* says nothing.
|
|
391
|
+
*
|
|
392
|
+
* @param {object} options
|
|
393
|
+
* @param {string} options.root - The brand root the lane snapshots.
|
|
394
|
+
* @returns {void}
|
|
395
|
+
* @throws {Error} A refusal naming each disagreeing entry and `omega i live`.
|
|
396
|
+
*/
|
|
397
|
+
function assertBrandLockfile({ root }) {
|
|
398
|
+
const { lockPath, lock, drift } = brandLockfileDrift({ root });
|
|
399
|
+
const fix = ' fix: run `omega i live` in the brand, which regenerates the brand\'s own lockfile from its manifests (#938)';
|
|
400
|
+
|
|
401
|
+
if (!lock) {
|
|
402
|
+
throw refusal(
|
|
403
|
+
`${root} has no package-lock.json, and the deploy runner installs with \`npm ci\`, which needs one.\n${fix}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (drift.length) {
|
|
408
|
+
throw refusal(
|
|
409
|
+
`${lockPath} disagrees with the manifests, and the deploy runner's \`npm ci\` installs the lock:\n`
|
|
410
|
+
+ drift.map((item) => ` ${item.manifest}: ${item.name} ${item.spec} ${item.reason}\n`).join('')
|
|
411
|
+
+ fix,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = {
|
|
417
|
+
readBrandVersion,
|
|
418
|
+
listTargetPackages,
|
|
419
|
+
bumpBrandVersion,
|
|
420
|
+
assertBrandVersion,
|
|
421
|
+
staleLockPaths,
|
|
422
|
+
brandLockfileDrift,
|
|
423
|
+
assertBrandLockfile,
|
|
424
|
+
KINDS,
|
|
425
|
+
};
|
|
@@ -14,6 +14,7 @@ const { execSync, execFileSync } = require('node:child_process');
|
|
|
14
14
|
|
|
15
15
|
const { findBrandRoot, discoverTargets, frameworkPackagesOf } = require('./local.js');
|
|
16
16
|
const { stageLocalPackages, STAGING_DIR } = require('./pack-local.js');
|
|
17
|
+
const { assertBrandLockfile } = require('./brand-version.js');
|
|
17
18
|
const {
|
|
18
19
|
pushSnapshot,
|
|
19
20
|
pushWorkflowFiles,
|
|
@@ -27,7 +28,8 @@ const {
|
|
|
27
28
|
} = require('./deploy-snapshot.js');
|
|
28
29
|
// The remote-url parse this module published before the boot prelude needed it
|
|
29
30
|
// too (#890): it lives in the remote boundary now, re-exported here unchanged.
|
|
30
|
-
|
|
31
|
+
// The origin gate (#934) lives there too, beside the one origin read.
|
|
32
|
+
const { parseRemoteUrl, assertOriginMatches } = require('./git-remote.js');
|
|
31
33
|
|
|
32
34
|
const API_BASE = 'https://api.github.com';
|
|
33
35
|
|
|
@@ -213,6 +215,8 @@ function laneLabel(lane, snapshot) {
|
|
|
213
215
|
// `deploy-precheck` uses): the tests drive the order without a push, a pack or
|
|
214
216
|
// a network call, and the defaults are the real modules.
|
|
215
217
|
const LANE_STEPS = {
|
|
218
|
+
origin: (options) => assertOriginMatches({ dir: options.root }),
|
|
219
|
+
lockfile: assertBrandLockfile,
|
|
216
220
|
defaultBranch: defaultBranchOf,
|
|
217
221
|
heal: healDefaultBranch,
|
|
218
222
|
behind: assertNotBehind,
|
|
@@ -223,6 +227,42 @@ const LANE_STEPS = {
|
|
|
223
227
|
wait: waitForWorkflow,
|
|
224
228
|
};
|
|
225
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The origin gate ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)):
|
|
232
|
+
* a brand whose `origin` names another repo than the source repo its config
|
|
233
|
+
* derives refuses, because every lane acts on the DERIVED repo (the snapshot
|
|
234
|
+
* push, the workflow compose, the dispatch). It runs on every lane, the
|
|
235
|
+
* dispatch-only one included: a brand outside git has no origin to disagree,
|
|
236
|
+
* which the gate answers as nothing to compare. Read-only, so the dry run runs
|
|
237
|
+
* it too.
|
|
238
|
+
*
|
|
239
|
+
* @param {object} lane - The resolved lane.
|
|
240
|
+
* @param {object} steps - The lane steps (`origin` is the gate).
|
|
241
|
+
* @returns {void}
|
|
242
|
+
* @throws {Error} The drift line, naming `repo.org`.
|
|
243
|
+
*/
|
|
244
|
+
function checkLaneOrigin(lane, steps) {
|
|
245
|
+
steps.origin({ root: lane.brandRoot });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The lockfile gate, where the lane ships the brand's OWN lock
|
|
250
|
+
* ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)): a registry
|
|
251
|
+
* (unlinked) snapshot. A linked lane regenerates its lock in the pack step, and
|
|
252
|
+
* a lane with no snapshot ships nothing, so neither has a lock of its own to
|
|
253
|
+
* check. Read-only, so the dry run runs it too.
|
|
254
|
+
*
|
|
255
|
+
* @param {object} lane - The resolved lane.
|
|
256
|
+
* @param {object} steps - The lane steps (`lockfile` is the gate).
|
|
257
|
+
* @returns {void}
|
|
258
|
+
* @throws {Error} The gate's refusal, naming `omega i live`.
|
|
259
|
+
*/
|
|
260
|
+
function checkLaneLockfile(lane, steps) {
|
|
261
|
+
if (lane.mode === 'snapshot' && !lane.linked) {
|
|
262
|
+
steps.lockfile({ root: lane.brandRoot });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
226
266
|
/**
|
|
227
267
|
* The DELIVERY half of a lane: how this brand's code reaches GitHub before the
|
|
228
268
|
* dispatch that runs it. ONE implementation for its two callers
|
|
@@ -233,6 +273,13 @@ const LANE_STEPS = {
|
|
|
233
273
|
*
|
|
234
274
|
* What the lane delivers, in order
|
|
235
275
|
* ([#915](https://github.com/Omega-JS-Stack/omega/issues/915)):
|
|
276
|
+
* 0. on EVERY lane, the ORIGIN gate
|
|
277
|
+
* ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)): a checkout
|
|
278
|
+
* whose `origin` is not the derived source repo refuses before this lane
|
|
279
|
+
* writes to that repo; then, on a registry (unlinked) lane, the LOCKFILE gate
|
|
280
|
+
* ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)): the runner's
|
|
281
|
+
* `npm ci` installs the brand's lock as pushed, so one that disagrees with
|
|
282
|
+
* the manifests refuses here, before a branch is healed or a file written;
|
|
236
283
|
* 1. the DEFAULT branch read once, and healed when published output has taken
|
|
237
284
|
* it over ([#922](https://github.com/Omega-JS-Stack/omega/issues/922)): a
|
|
238
285
|
* `gh-pages` default moves back to `main` here, before step 2 writes a
|
|
@@ -271,10 +318,14 @@ async function deliverLane(options) {
|
|
|
271
318
|
const steps = { ...LANE_STEPS, ...(options.steps || {}) };
|
|
272
319
|
const ref = options.ref || lane.ref;
|
|
273
320
|
|
|
321
|
+
checkLaneOrigin(lane, steps);
|
|
322
|
+
|
|
274
323
|
if (lane.mode !== 'snapshot') {
|
|
275
324
|
return { sha: null };
|
|
276
325
|
}
|
|
277
326
|
|
|
327
|
+
checkLaneLockfile(lane, steps);
|
|
328
|
+
|
|
278
329
|
// Read ONCE, used twice: the branch a behind checkout is measured against,
|
|
279
330
|
// and the branch the composed workflows are pushed to. Null is a repo GitHub
|
|
280
331
|
// does not have yet, and the snapshot push below is where that says itself.
|
|
@@ -428,9 +479,19 @@ async function deployViaDispatch(options) {
|
|
|
428
479
|
inputs: options.inputs,
|
|
429
480
|
});
|
|
430
481
|
|
|
482
|
+
const steps = { ...LANE_STEPS, ...(options.steps || {}) };
|
|
483
|
+
|
|
431
484
|
if (options.dryRun) {
|
|
485
|
+
// The origin and lockfile gates only read, so a dry run refuses what the
|
|
486
|
+
// real run would (#934, #938), as the version gate already does.
|
|
487
|
+
if (lane) {
|
|
488
|
+
checkLaneOrigin(lane, steps);
|
|
489
|
+
checkLaneLockfile(lane, steps);
|
|
490
|
+
}
|
|
491
|
+
|
|
432
492
|
// The plan IS the dry run, the lane included: what would happen, with
|
|
433
|
-
//
|
|
493
|
+
// nothing written to git and the network untouched (the gates above only
|
|
494
|
+
// read). The workflow half of it is read off
|
|
434
495
|
// DISK (the scaffold has already composed this run's files), so the preview
|
|
435
496
|
// names them and the branch they would go to without asking GitHub which of
|
|
436
497
|
// them differ (#915).
|
|
@@ -446,7 +507,6 @@ async function deployViaDispatch(options) {
|
|
|
446
507
|
}
|
|
447
508
|
|
|
448
509
|
const token = options.token || resolveToken({ env: options.env, execFn: options.execFn });
|
|
449
|
-
const steps = { ...LANE_STEPS, ...(options.steps || {}) };
|
|
450
510
|
|
|
451
511
|
if (lane && lane.mode === 'snapshot' && options.snapshot) {
|
|
452
512
|
// The brand root pushed this run's snapshot ONCE, before it spawned a
|
|
@@ -539,7 +599,7 @@ function findLocalSpecs(options = {}) {
|
|
|
539
599
|
* - NESTED: the brand root is not the toplevel of the git repo it sits in (a
|
|
540
600
|
* brand inside this monorepo), so its git toplevel is somebody else's repo:
|
|
541
601
|
* the behind check has nothing to compare, and the secrets publisher skips
|
|
542
|
-
* its
|
|
602
|
+
* its origin gate.
|
|
543
603
|
* - LINKED: the brand tree carries a `file:` @omega.js spec anywhere. A runner
|
|
544
604
|
* can install none of those, so the packed tarballs have to travel with the
|
|
545
605
|
* snapshot.
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* pack and snapshot modules: 40ms nothing on the CLI BOOT path can spend for
|
|
7
7
|
* one regex. The boot prelude that heals a redirected `origin` needs the parse
|
|
8
8
|
* plus the two git calls around it, so the three live here, in one place, and
|
|
9
|
-
* `deploy.js` re-exports the parse it already published.
|
|
9
|
+
* `deploy.js` re-exports the parse it already published. The brand's own origin,
|
|
10
|
+
* read once for the prelude, the manage walk and the deploy alike, and the
|
|
11
|
+
* refusal the latter two raise when it disagrees with the config, live here too
|
|
12
|
+
* ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)).
|
|
10
13
|
*
|
|
11
14
|
* Transport: `git` through execFile with an ARGV ARRAY, never a shell string,
|
|
12
15
|
* so a remote url can never inject a command. Every call names its directory
|
|
@@ -15,7 +18,9 @@
|
|
|
15
18
|
* remote.
|
|
16
19
|
*/
|
|
17
20
|
|
|
21
|
+
const path = require('node:path');
|
|
18
22
|
const { execFileSync } = require('node:child_process');
|
|
23
|
+
const jetpack = require('fs-jetpack');
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Parse a git remote URL into { owner, repo }.
|
|
@@ -87,9 +92,81 @@ function setRemoteUrl(options) {
|
|
|
87
92
|
execFn('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* A brand's OWN `origin`, read and parsed: the one read the boot prelude, the
|
|
97
|
+
* manage walk and the deploy share
|
|
98
|
+
* ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)).
|
|
99
|
+
*
|
|
100
|
+
* The `.git` must sit AT the directory, answered by one stat before git is
|
|
101
|
+
* asked anything: a brand nested in another repo (a fixture brand inside this
|
|
102
|
+
* monorepo, a target checked out under someone else's tree) has no origin of
|
|
103
|
+
* its own, and reading the ENCLOSING repo's would compare, heal or refuse on a
|
|
104
|
+
* repo the brand is not. No origin, and an origin that is not a GitHub remote,
|
|
105
|
+
* are expected conditions (a checkout nobody has pushed yet, a brand hosted
|
|
106
|
+
* elsewhere), so each answers its reason rather than throwing.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} options
|
|
109
|
+
* @param {string} options.dir - The brand root.
|
|
110
|
+
* @param {function} [options.execFn] - Injectable `(file, args, opts) => stdout` (tests).
|
|
111
|
+
* @returns {{ url: string, owner: string, repo: string, slug: string }|{ reason: 'no-git'|'no-origin'|'foreign-remote' }}
|
|
112
|
+
*/
|
|
113
|
+
function readOrigin(options) {
|
|
114
|
+
if (!jetpack.exists(path.join(options.dir, '.git'))) return { reason: 'no-git' };
|
|
115
|
+
|
|
116
|
+
let url;
|
|
117
|
+
try {
|
|
118
|
+
url = remoteUrl({ dir: options.dir, execFn: options.execFn });
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { reason: 'no-origin' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parsed = parseRemoteUrl(url);
|
|
124
|
+
if (!parsed) return { reason: 'foreign-remote' };
|
|
125
|
+
|
|
126
|
+
return { url, owner: parsed.owner, repo: parsed.repo, slug: `${parsed.owner}/${parsed.repo}` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Refuse when the brand's `origin` names another repo than the source repo its
|
|
131
|
+
* config derives ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)).
|
|
132
|
+
* Called by the verbs that ACT on the derived repo (the manage walk's repo
|
|
133
|
+
* service, the deploy lane), which would otherwise ensure or push to a repo the
|
|
134
|
+
* checkout does not point at. The boot prelude only states the same line: it
|
|
135
|
+
* runs before every verb, and a fatal boot would lock out the verbs that fix it.
|
|
136
|
+
*
|
|
137
|
+
* Nothing to compare is no refusal: a brand with no origin of its own (see
|
|
138
|
+
* `readOrigin`), and a config that derives no source repo.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} options
|
|
141
|
+
* @param {string} options.dir - The brand root.
|
|
142
|
+
* @param {object} [options.config] - The composed config. Omitted, the brand's
|
|
143
|
+
* PRODUCTION config is loaded from `dir` (the one every deploy address reads,
|
|
144
|
+
* [#856](https://github.com/Omega-JS-Stack/omega/issues/856)), and only once
|
|
145
|
+
* an origin exists to compare it with.
|
|
146
|
+
* @param {function} [options.execFn] - Injectable git exec (tests).
|
|
147
|
+
* @returns {object} The `readOrigin` answer it compared (or had nothing to
|
|
148
|
+
* compare with), so a caller that treats a missing origin its own way reads
|
|
149
|
+
* the reason without a second read.
|
|
150
|
+
* @throws {Error} The drift line, from `@omega.js/config`'s `repoDrift`.
|
|
151
|
+
*/
|
|
152
|
+
function assertOriginMatches(options) {
|
|
153
|
+
const origin = readOrigin(options);
|
|
154
|
+
if (!origin.slug) return origin;
|
|
155
|
+
|
|
156
|
+
const { loadConfig, repoDrift } = require('../config/index.js');
|
|
157
|
+
const config = options.config || loadConfig(options.dir, null, { environment: 'production' }).config;
|
|
158
|
+
const drift = repoDrift(origin.slug, config);
|
|
159
|
+
|
|
160
|
+
if (drift) throw new Error(drift);
|
|
161
|
+
|
|
162
|
+
return origin;
|
|
163
|
+
}
|
|
164
|
+
|
|
90
165
|
module.exports = {
|
|
91
166
|
parseRemoteUrl,
|
|
92
167
|
retargetRemoteUrl,
|
|
93
168
|
remoteUrl,
|
|
94
169
|
setRemoteUrl,
|
|
170
|
+
readOrigin,
|
|
171
|
+
assertOriginMatches,
|
|
95
172
|
};
|
|
@@ -261,6 +261,33 @@ function relativeSpecPath(fromDir, target) {
|
|
|
261
261
|
return path.relative(fs.realpathSync(fromDir), fs.realpathSync(target)).split(path.sep).join('/');
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Regenerate the install root's OWN lockfile after a flip (#938): inside the
|
|
266
|
+
* omega monorepo the tree install climbs to the monorepo root and writes that
|
|
267
|
+
* lockfile, so the brand's would stay stale. Loaded here rather than at the top:
|
|
268
|
+
* this module runs at every CLI boot (freshnessBoot), and the lockfile read
|
|
269
|
+
* pulls in @omega.js/config, which the boot has no use for.
|
|
270
|
+
* @param {string} installRoot - The brand root (or standalone target).
|
|
271
|
+
* @param {object} [logger] - Logger with log (silent when omitted).
|
|
272
|
+
* @returns {Promise<void>}
|
|
273
|
+
*/
|
|
274
|
+
function regenerateBrandLockfile(installRoot, logger) {
|
|
275
|
+
const { regenerateLockfile } = require('./lockfile.js');
|
|
276
|
+
return regenerateLockfile({ root: installRoot, log: logger ? (line) => logger.log(line) : undefined });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Whether the install root's lockfile is missing or disagrees with its
|
|
281
|
+
* manifests' @omega.js registry specs: the deploy gate's own read (#938).
|
|
282
|
+
* @param {string} installRoot - The brand root (or standalone target).
|
|
283
|
+
* @returns {boolean}
|
|
284
|
+
*/
|
|
285
|
+
function brandLockfileStale(installRoot) {
|
|
286
|
+
const { brandLockfileDrift } = require('./brand-version.js');
|
|
287
|
+
const { lock, drift } = brandLockfileDrift({ root: installRoot });
|
|
288
|
+
return !lock || drift.length > 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
264
291
|
/**
|
|
265
292
|
* file:-install a brand's @omega.js dependencies from the local monorepo.
|
|
266
293
|
*
|
|
@@ -275,7 +302,8 @@ function relativeSpecPath(fromDir, target) {
|
|
|
275
302
|
* targets degenerate to themselves.
|
|
276
303
|
*
|
|
277
304
|
* Idempotent: dependencies already resolving to the monorepo copy are
|
|
278
|
-
* skipped, and when nothing needs linking no install runs.
|
|
305
|
+
* skipped, and when nothing needs linking no install runs. An install is
|
|
306
|
+
* followed by the brand's OWN lockfile regeneration (#938).
|
|
279
307
|
* @param {object} options
|
|
280
308
|
* @param {string} options.dir - Target directory to link from (any target in the brand).
|
|
281
309
|
* @param {string} options.monorepoRoot - Monorepo root path.
|
|
@@ -347,6 +375,11 @@ async function linkLocalPackages(options) {
|
|
|
347
375
|
logger && logger.warn(`install failed — restored ${manifestBackups.size} manifest(s) to their pre-link specs`);
|
|
348
376
|
throw error;
|
|
349
377
|
}
|
|
378
|
+
|
|
379
|
+
// Outside the rollback: the install succeeded, so the manifests and the
|
|
380
|
+
// tree already agree, and restoring the manifests now would split them.
|
|
381
|
+
// A failed regeneration puts the lock back itself and throws as what it is.
|
|
382
|
+
await regenerateBrandLockfile(installRoot, logger);
|
|
350
383
|
}
|
|
351
384
|
|
|
352
385
|
return actions;
|
|
@@ -364,7 +397,9 @@ async function linkLocalPackages(options) {
|
|
|
364
397
|
* @omega.js/client in one brand. An explicit `range` still wins verbatim — the
|
|
365
398
|
* caller owns that string. Idempotent: registry-spec'd entries are untouched;
|
|
366
399
|
* nothing to flip → no install. Same transactional manifest restore as the
|
|
367
|
-
* linker when the install fails.
|
|
400
|
+
* linker when the install fails. Either way the brand's OWN lockfile ends up
|
|
401
|
+
* agreeing with the manifests (#938): regenerated after the install, and
|
|
402
|
+
* regenerated alone when nothing flipped but the lock still disagrees.
|
|
368
403
|
* @param {object} options
|
|
369
404
|
* @param {string} options.dir - Any directory inside the brand.
|
|
370
405
|
* @param {object} [options.logger] - Logger with log/warn (silent when omitted).
|
|
@@ -416,7 +451,11 @@ async function restoreRegistrySpecs(options) {
|
|
|
416
451
|
}
|
|
417
452
|
}
|
|
418
453
|
|
|
419
|
-
if (
|
|
454
|
+
if (dryRun) {
|
|
455
|
+
return actions;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (installNeeded) {
|
|
420
459
|
try {
|
|
421
460
|
await safeInstall('npm install', { log: true, config: { cwd: installRoot } });
|
|
422
461
|
} catch (error) {
|
|
@@ -426,6 +465,15 @@ async function restoreRegistrySpecs(options) {
|
|
|
426
465
|
logger && logger.warn(`install failed — restored ${manifestBackups.size} manifest(s) to their file: specs`);
|
|
427
466
|
throw error;
|
|
428
467
|
}
|
|
468
|
+
|
|
469
|
+
// Outside the rollback, as in linkLocalPackages: the flipped manifests and
|
|
470
|
+
// the installed tree agree, so only the lock is left to heal.
|
|
471
|
+
await regenerateBrandLockfile(installRoot, logger);
|
|
472
|
+
} else if (brandLockfileStale(installRoot)) {
|
|
473
|
+
// Nothing to flip, but the lock still disagrees (the specs were flipped
|
|
474
|
+
// before the lock was ever regenerated): this verb is the fix the deploy's
|
|
475
|
+
// lockfile gate names, so it heals the lock here too (#938).
|
|
476
|
+
await regenerateBrandLockfile(installRoot, logger);
|
|
429
477
|
}
|
|
430
478
|
|
|
431
479
|
return actions;
|