@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lockfile: regenerate an install root's OWN package-lock.json
|
|
3
|
+
* ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)).
|
|
4
|
+
*
|
|
5
|
+
* ONE helper for its three callers: the pack step (`pack-local`, the staged
|
|
6
|
+
* shape), `omega i local` (`linkLocalPackages`) and `omega i live`
|
|
7
|
+
* (`restoreRegistrySpecs`).
|
|
8
|
+
*
|
|
9
|
+
* Two things a plain `npm install` gets wrong here, and this answers both:
|
|
10
|
+
* - `--prefix` makes npm treat `root` as the install root. Without it, npm in
|
|
11
|
+
* a directory that is itself a WORKSPACE of an outer root (every in-repo
|
|
12
|
+
* brand, under the omega monorepo's `brands/*`) climbs to that outer root
|
|
13
|
+
* and writes ITS lockfile, leaving the brand's own untouched.
|
|
14
|
+
* - npm KEEPS a locked link whose target's version satisfies a registry spec,
|
|
15
|
+
* so a lock from the local era (`link: true` into a checkout already at the
|
|
16
|
+
* published number) would survive the flip. Every @omega.js entry the
|
|
17
|
+
* deploy gate would refuse (`brandLockfileDrift`) is dropped first, with its
|
|
18
|
+
* link target, so npm resolves those afresh and keeps every other pin.
|
|
19
|
+
* - npm also honors a STALE path entry, a folder the lock names that is no
|
|
20
|
+
* longer on disk (a renamed target), and re-creates whatever link its
|
|
21
|
+
* dependencies declare. Those entries go first, with everything under them
|
|
22
|
+
* and every `node_modules/` link pointing into them.
|
|
23
|
+
*
|
|
24
|
+
* Transactional: a failed npm run writes the lock back byte for byte.
|
|
25
|
+
*/
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const jetpack = require('fs-jetpack');
|
|
29
|
+
|
|
30
|
+
const { safeInstall } = require('./safe-install.js');
|
|
31
|
+
const { brandLockfileDrift, staleLockPaths } = require('./brand-version.js');
|
|
32
|
+
|
|
33
|
+
/** A path equal to base, or nested beneath it. */
|
|
34
|
+
const under = (value, base) => value === base || value.startsWith(`${base}/`);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Drop every stale path entry (`staleLockPaths`), everything nested under it,
|
|
38
|
+
* and every `node_modules/` entry whose `resolved` points into it.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} lock - The parsed lock (mutated).
|
|
41
|
+
* @param {string} root - The install root the keys are relative to.
|
|
42
|
+
* @returns {string[]} The stale paths dropped (the outermost of each tree).
|
|
43
|
+
*/
|
|
44
|
+
function dropStalePaths(lock, root) {
|
|
45
|
+
const stale = staleLockPaths({ root, packages: lock.packages });
|
|
46
|
+
const outermost = stale.filter((key) => !stale.some((other) => other !== key && under(key, other)));
|
|
47
|
+
|
|
48
|
+
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
49
|
+
const pointsInto = entry && typeof entry.resolved === 'string' && outermost.some((base) => under(entry.resolved, base));
|
|
50
|
+
if (outermost.some((base) => under(key, base)) || pointsInto) delete lock.packages[key];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return outermost;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Drop every lock entry `drift` names, plus a dropped link's target entry and
|
|
58
|
+
* everything nested under it (the checkout's own dependencies).
|
|
59
|
+
*
|
|
60
|
+
* @param {object} lock - The parsed lock (mutated).
|
|
61
|
+
* @param {Array<{ key: string|null, entry: object|undefined }>} drift - What the gate refused.
|
|
62
|
+
* @returns {string[]} The names whose entries were dropped.
|
|
63
|
+
*/
|
|
64
|
+
function dropDrift(lock, drift) {
|
|
65
|
+
const dropped = [];
|
|
66
|
+
|
|
67
|
+
for (const { name, key, entry } of drift) {
|
|
68
|
+
// An absent entry has nothing to drop (npm adds it from the manifest), and
|
|
69
|
+
// one inside a stale path is already gone with it.
|
|
70
|
+
if (!key || !lock.packages[key]) continue;
|
|
71
|
+
|
|
72
|
+
delete lock.packages[key];
|
|
73
|
+
dropped.push(name);
|
|
74
|
+
|
|
75
|
+
if (entry.link && entry.resolved) {
|
|
76
|
+
for (const other of Object.keys(lock.packages)) {
|
|
77
|
+
if (under(other, entry.resolved)) delete lock.packages[other];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return dropped;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Regenerate `root`'s package-lock.json from its manifests, lock-only: no
|
|
87
|
+
* scripts, no node_modules writes.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} options
|
|
90
|
+
* @param {string} options.root - The install root whose lock is regenerated.
|
|
91
|
+
* @param {function} [options.log] - Line sink (silent when omitted).
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
async function regenerateLockfile({ root, log }) {
|
|
95
|
+
const { lockPath, lock, drift } = brandLockfileDrift({ root });
|
|
96
|
+
const original = lock ? jetpack.read(lockPath) : null;
|
|
97
|
+
|
|
98
|
+
if (lock && lock.packages) {
|
|
99
|
+
const pruned = dropStalePaths(lock, root);
|
|
100
|
+
const dropped = dropDrift(lock, drift);
|
|
101
|
+
|
|
102
|
+
if (pruned.length || dropped.length) {
|
|
103
|
+
jetpack.write(lockPath, `${JSON.stringify(lock, null, 2)}\n`);
|
|
104
|
+
}
|
|
105
|
+
if (log && pruned.length) log(`Pruned the lock entries of folders no longer on disk (${pruned.join(', ')}) so npm stops honoring what they declare`);
|
|
106
|
+
if (log && dropped.length) log(`Dropped the stale lock entries of ${dropped.join(', ')} so npm resolves them from the manifests`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (log) log(`Regenerating ${path.basename(root)}/package-lock.json...`);
|
|
110
|
+
|
|
111
|
+
// The REAL path: npm writes a link's `resolved` relative to the physical
|
|
112
|
+
// directory, so a prefix spelled through an alias (macOS /var for
|
|
113
|
+
// /private/var, a symlinked brand dir) reads those paths from the wrong place.
|
|
114
|
+
const prefix = fs.realpathSync(root);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await safeInstall(
|
|
118
|
+
`npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix "${prefix}"`,
|
|
119
|
+
{ log: false, config: { cwd: prefix } },
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (original !== null) jetpack.write(lockPath, original);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { regenerateLockfile };
|
|
@@ -47,6 +47,7 @@ const path = require('path');
|
|
|
47
47
|
const jetpack = require('fs-jetpack');
|
|
48
48
|
const powertools = require('node-powertools');
|
|
49
49
|
const chalk = require('chalk').default;
|
|
50
|
+
const { regenerateLockfile } = require('./lockfile.js');
|
|
50
51
|
|
|
51
52
|
// Constants
|
|
52
53
|
const STAGING_DIR = 'omega_modules';
|
|
@@ -369,15 +370,11 @@ async function stageLocalPackages({ dir, log = () => {} }) {
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
// A remote install runs `npm ci`, so the uploaded lockfile must match the
|
|
372
|
-
// staged shape
|
|
373
|
-
//
|
|
374
|
-
// root and write that one's lockfile instead.
|
|
373
|
+
// staged shape, regenerated from scratch (the one helper keeps it THIS
|
|
374
|
+
// dir's lock, never an enclosing workspace root's).
|
|
375
375
|
log(` ${chalk.dim('→')} Regenerating package-lock.json for the staged shape...`);
|
|
376
376
|
jetpack.remove(lockPath);
|
|
377
|
-
await
|
|
378
|
-
`npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix "${root}"`,
|
|
379
|
-
{ log: false, config: { cwd: root } },
|
|
380
|
-
);
|
|
377
|
+
await regenerateLockfile({ root });
|
|
381
378
|
} catch (error) {
|
|
382
379
|
// Loud and clean: the deploy stops here, and the tree goes back exactly as
|
|
383
380
|
// it was rather than sitting half-staged.
|
|
@@ -32,21 +32,22 @@
|
|
|
32
32
|
* - anything else: the remote is rewritten to the answered `full_name`,
|
|
33
33
|
* keeping its own url form, and the heal is stated in one line.
|
|
34
34
|
*
|
|
35
|
-
* Then DRIFT, on whatever the answer was:
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* and
|
|
35
|
+
* Then DRIFT, on whatever the answer was: the WHOLE slug GitHub answers against
|
|
36
|
+
* the source repo the config derives (`@omega.js/config`'s `repoDrift`, so a
|
|
37
|
+
* rename drifts as surely as a transfer,
|
|
38
|
+
* [#934](https://github.com/Omega-JS-Stack/omega/issues/934)), stated in one
|
|
39
|
+
* line and nothing more. Never fatal: this runs before every verb, and a fatal
|
|
40
|
+
* boot would lock out the verbs that fix the mismatch. The verbs that ACT on
|
|
41
|
+
* the derived repo (the manage walk, the deploy) refuse on the same line
|
|
42
|
+
* instead. The prelude never writes config: which of the two is wrong (the
|
|
43
|
+
* config, or where the repo lives) is a human's call.
|
|
40
44
|
*
|
|
41
45
|
* The WRITE is the one thing that fails loudly: a `git remote set-url` refused
|
|
42
46
|
* after a `get-url` just answered is not an external condition, it is a broken
|
|
43
47
|
* invariant, and the runner stops the boot on it.
|
|
44
48
|
*/
|
|
45
49
|
|
|
46
|
-
const
|
|
47
|
-
const jetpack = require('fs-jetpack');
|
|
48
|
-
|
|
49
|
-
const { parseRemoteUrl, retargetRemoteUrl, remoteUrl, setRemoteUrl } = require('../git-remote.js');
|
|
50
|
+
const { readOrigin, retargetRemoteUrl, setRemoteUrl } = require('../git-remote.js');
|
|
50
51
|
const github = require('../github-repo.js');
|
|
51
52
|
|
|
52
53
|
// The one network read runs on EVERY verb boot, so a network that hangs (a
|
|
@@ -61,7 +62,7 @@ const GITHUB_READ_TIMEOUT_MS = 10000;
|
|
|
61
62
|
* @param {function} [context.execFn] - Injectable git exec (tests).
|
|
62
63
|
* @param {function} [context.resolveRepo] - Injectable `(owner, name) => repo|null` (tests).
|
|
63
64
|
* @param {function} [context.log] - Injectable line printer (tests).
|
|
64
|
-
* @returns {{ healed: boolean, reason?: string, from?: string, to?: string, drift?: {
|
|
65
|
+
* @returns {{ healed: boolean, reason?: string, from?: string, to?: string, drift?: { origin: string, derived: string } }}
|
|
65
66
|
*/
|
|
66
67
|
function run(context = {}) {
|
|
67
68
|
const { brandRoot, execFn } = context;
|
|
@@ -72,21 +73,12 @@ function run(context = {}) {
|
|
|
72
73
|
|
|
73
74
|
if (!brandRoot) return { healed: false, reason: 'no-brand' };
|
|
74
75
|
|
|
75
|
-
// One stat
|
|
76
|
-
// in every fixture brand end
|
|
77
|
-
|
|
76
|
+
// One stat before anything else (most boots in this monorepo's own trees and
|
|
77
|
+
// in every fixture brand end there), then the remote itself.
|
|
78
|
+
const current = readOrigin({ dir: brandRoot, execFn });
|
|
79
|
+
if (!current.slug) return { healed: false, reason: current.reason };
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
url = remoteUrl({ dir: brandRoot, execFn });
|
|
82
|
-
} catch (e) {
|
|
83
|
-
return { healed: false, reason: 'no-origin' };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const current = parseRemoteUrl(url);
|
|
87
|
-
if (!current) return { healed: false, reason: 'foreign-remote' };
|
|
88
|
-
|
|
89
|
-
const from = `${current.owner}/${current.repo}`;
|
|
81
|
+
const { url, slug: from } = current;
|
|
90
82
|
|
|
91
83
|
// The ONE network read, on the slug the checkout carries: GitHub follows its
|
|
92
84
|
// own redirect and names where that repo lives now.
|
|
@@ -109,44 +101,38 @@ function run(context = {}) {
|
|
|
109
101
|
result = { healed: true, from, to };
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
const
|
|
104
|
+
const config = composedConfig(brandRoot, context.config);
|
|
105
|
+
const { repoDrift, sourceRepo } = require('../../config/index.js');
|
|
106
|
+
const drift = config ? repoDrift(to, config) : null;
|
|
107
|
+
|
|
113
108
|
if (drift) {
|
|
114
|
-
log(`omega:
|
|
115
|
-
result.drift =
|
|
109
|
+
log(`omega: ${drift}`);
|
|
110
|
+
result.drift = { origin: to, derived: sourceRepo(config).slug };
|
|
116
111
|
}
|
|
117
112
|
|
|
118
113
|
return result;
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
/**
|
|
122
|
-
* The
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* declaring no `repo` block types no org, so there is nothing to disagree with.
|
|
117
|
+
* The composed config the drift is read against: the caller's when it has one,
|
|
118
|
+
* else loaded here, and an unloadable one answers null ("no drift"): a config
|
|
119
|
+
* the boot cannot read is the VERB's failure to report, in its own words, never
|
|
120
|
+
* a prelude's.
|
|
127
121
|
*
|
|
128
122
|
* @param {string} brandRoot - The brand root.
|
|
129
123
|
* @param {object} [config] - The composed config, when the caller has it.
|
|
130
|
-
* @
|
|
131
|
-
* @returns {{ owner: string, org: string }|null}
|
|
124
|
+
* @returns {object|null}
|
|
132
125
|
*/
|
|
133
|
-
function
|
|
134
|
-
|
|
126
|
+
function composedConfig(brandRoot, config) {
|
|
127
|
+
if (config) return config;
|
|
135
128
|
|
|
136
|
-
|
|
137
|
-
if (config) {
|
|
138
|
-
block = repoBlock(config);
|
|
139
|
-
} else {
|
|
140
|
-
try {
|
|
141
|
-
block = repoBlock(loadConfig(brandRoot).config);
|
|
142
|
-
} catch (e) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (!block || block.org.toLowerCase() === owner.toLowerCase()) return null;
|
|
129
|
+
const { loadConfig } = require('../../config/index.js');
|
|
148
130
|
|
|
149
|
-
|
|
131
|
+
try {
|
|
132
|
+
return loadConfig(brandRoot).config;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
150
136
|
}
|
|
151
137
|
|
|
152
138
|
module.exports = {
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
* config (`repo.org`, the SOURCE repo). An inferred git remote is not proof: a
|
|
32
32
|
* target vendored into a framework/test monorepo, a cloned starter still
|
|
33
33
|
* pointing at the template author, or any fork would publish this brand's
|
|
34
|
-
* `.env` to a stranger's Actions.
|
|
34
|
+
* `.env` to a stranger's Actions. A checkout that could be the brand's own
|
|
35
|
+
* and whose origin names another repo REFUSES on `repoDrift`'s one line
|
|
36
|
+
* ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)), the
|
|
37
|
+
* same refusal the deploy lane and the manage walk make.
|
|
35
38
|
* 4. PUBLISH — each collected key becomes a repo Actions secret via devkit's
|
|
36
39
|
* `gh` boundary (values on stdin, never logged).
|
|
37
40
|
*
|
|
@@ -70,11 +73,12 @@
|
|
|
70
73
|
* sends nothing. The refusal still THROWS in a dry run: a plan that cannot be
|
|
71
74
|
* made is loud.
|
|
72
75
|
*/
|
|
73
|
-
const { composeTargetEnv, loadConfig } = require('../config/index.js');
|
|
76
|
+
const { composeTargetEnv, loadConfig, sourceRepo } = require('../config/index.js');
|
|
74
77
|
const { publishSecretKeys } = require('../config/env-delivery.js');
|
|
75
78
|
const { checkEnvRules } = require('../config/env-rules.js');
|
|
76
79
|
const { publishActionsSecrets } = require('./actions-secrets.js');
|
|
77
|
-
const {
|
|
80
|
+
const { assertOriginMatches } = require('./git-remote.js');
|
|
81
|
+
const { findBrandRoot } = require('./local.js');
|
|
78
82
|
const { targetSeams } = require('./target-seams.js');
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -200,26 +204,18 @@ function missingRequiredSecrets(options) {
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/**
|
|
203
|
-
* The
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* nothing usable or doesn't load.
|
|
207
|
+
* The target's resolved PRODUCTION config, like every other read this publish
|
|
208
|
+
* makes (#895): the repo a release's secrets belong to is the one the
|
|
209
|
+
* production config names. Null when it doesn't load.
|
|
207
210
|
*
|
|
208
211
|
* @param {object} options
|
|
209
212
|
* @param {string} options.targetDir - The target root.
|
|
210
213
|
* @param {string} options.target - Target name ('web', 'extension', …).
|
|
211
|
-
* @returns {
|
|
214
|
+
* @returns {object|null}
|
|
212
215
|
*/
|
|
213
|
-
function
|
|
216
|
+
function productionConfig(options) {
|
|
214
217
|
try {
|
|
215
|
-
|
|
216
|
-
// PRODUCTION, like every other read this publish makes (#895): the repo a
|
|
217
|
-
// release's secrets belong to is the one the production config names.
|
|
218
|
-
const { config } = loadConfig(options.targetDir, options.target, { environment: 'production' });
|
|
219
|
-
if (!config) return null;
|
|
220
|
-
|
|
221
|
-
const source = sourceRepo(config);
|
|
222
|
-
return source ? source.slug : null;
|
|
218
|
+
return loadConfig(options.targetDir, options.target, { environment: 'production' }).config || null;
|
|
223
219
|
} catch (e) {
|
|
224
220
|
return null;
|
|
225
221
|
}
|
|
@@ -241,7 +237,6 @@ function declaredBrandRepo(options) {
|
|
|
241
237
|
* @param {Object<string, string>} [options.extraSecrets] - Override for the
|
|
242
238
|
* target's already-valued keys, merged OVER the composed set (tests).
|
|
243
239
|
* @param {function} [options.execFn] - Injectable `gh` exec (tests).
|
|
244
|
-
* @param {function} [options.gitExecFn] - Injectable `git` exec for remote discovery (tests).
|
|
245
240
|
* @returns {{ skipped: string }|{ planned: string[] }|{ published: string[], failed: Array<object> }}
|
|
246
241
|
*/
|
|
247
242
|
function publishTargetSecrets(options) {
|
|
@@ -297,38 +292,32 @@ function publishTargetSecrets(options) {
|
|
|
297
292
|
return { skipped: 'no-secrets' };
|
|
298
293
|
}
|
|
299
294
|
|
|
300
|
-
const
|
|
301
|
-
|
|
295
|
+
const config = productionConfig({ targetDir, target });
|
|
296
|
+
const source = config ? sourceRepo(config) : null;
|
|
297
|
+
if (!source) {
|
|
302
298
|
logger.warn('Skipping secret publication: this brand names no GitHub repo in config (repo.org). Set it, then re-run `omega deploy`.');
|
|
303
299
|
return { skipped: 'no-declared-repo' };
|
|
304
300
|
}
|
|
305
301
|
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
} catch (e) {
|
|
322
|
-
logger.warn(`Skipping secret publication — no GitHub remote here (${e.message})`);
|
|
323
|
-
return { skipped: 'no-remote' };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (declared.toLowerCase() !== remote.toLowerCase()) {
|
|
327
|
-
logger.warn(`Skipping secret publication — the git remote here is ${remote}, but this brand's repo is ${declared}. Run \`omega deploy\` from the brand's own checkout.`);
|
|
328
|
-
return { skipped: 'repo-mismatch' };
|
|
329
|
-
}
|
|
302
|
+
// The ONE origin gate the deploy lane and the manage walk use
|
|
303
|
+
// ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)): the brand
|
|
304
|
+
// root's own origin (the local remote, which the boot prelude has already
|
|
305
|
+
// healed onto GitHub's redirect) must BE the derived source repo, or the
|
|
306
|
+
// publish REFUSES on the one drift line, because arming a repo that is not
|
|
307
|
+
// the derived one is the harm itself. No `.git` AT the brand root is nothing
|
|
308
|
+
// to compare: a NESTED brand's remote is the enclosing repo's by
|
|
309
|
+
// construction, and its deploy pushes to the derived repo regardless
|
|
310
|
+
// ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)). A checkout
|
|
311
|
+
// with no GitHub origin (nobody pushed it yet, or it is hosted elsewhere) has
|
|
312
|
+
// no repo to arm, so it skips.
|
|
313
|
+
const origin = assertOriginMatches({ dir: findBrandRoot(targetDir), config });
|
|
314
|
+
if (origin.reason === 'no-origin' || origin.reason === 'foreign-remote') {
|
|
315
|
+
logger.warn(`Skipping secret publication: no GitHub remote here (${origin.reason})`);
|
|
316
|
+
return { skipped: 'no-remote' };
|
|
330
317
|
}
|
|
331
318
|
|
|
319
|
+
const repo = source.slug;
|
|
320
|
+
|
|
332
321
|
// The PLAN, and nothing else (#895): what the composition answered, by NAME.
|
|
333
322
|
// It runs after the refusal and the two guards above on purpose, so a dry run
|
|
334
323
|
// is a real preview of whether the deploy would publish at all: every one of
|
|
@@ -347,4 +336,4 @@ function publishTargetSecrets(options) {
|
|
|
347
336
|
return result;
|
|
348
337
|
}
|
|
349
338
|
|
|
350
|
-
module.exports = { collectTargetSecrets,
|
|
339
|
+
module.exports = { collectTargetSecrets, missingRequiredSecrets, publishTargetSecrets };
|
package/docs/index.md
CHANGED
|
@@ -305,7 +305,7 @@ API references for each subsystem live in `docs/`. **Whenever you make a behavio
|
|
|
305
305
|
- [docs/installer-options.md](../docs/installer-options.md) — per-target installer config, defaults table
|
|
306
306
|
- [docs/signing.md](../docs/signing.md) — code signing for macOS + Windows
|
|
307
307
|
- [docs/releasing.md](../docs/releasing.md) — end-to-end release walkthrough
|
|
308
|
-
- [docs/runner.md](../docs/runner.md): Windows EV-token signing runner. The listener runs with a private HOME (`%LOCALAPPDATA%\omega-runner\home`, holding a real `.gitconfig`), written at install and healed by `start`/`restart`, so a job's `actions/checkout` never copies the box's symlinked `~/.gitconfig` into a junction it cannot read ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)). The same install writes the box's JOB GUARD ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a job-started hook beside the runner, pointed at through each registration's `ACTIONS_RUNNER_HOOK_JOB_STARTED`, that fails any job whose event is not a dispatch or whose repository or actor is not on the box's own allow lists
|
|
308
|
+
- [docs/runner.md](../docs/runner.md): Windows EV-token signing runner. The listener runs with a private HOME (`%LOCALAPPDATA%\omega-runner\home`, holding a real `.gitconfig`), written at install and healed by `start`/`restart`, so a job's `actions/checkout` never copies the box's symlinked `~/.gitconfig` into a junction it cannot read ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)). The same install writes the box's JOB GUARD ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a job-started hook beside the runner, pointed at through each registration's `ACTIONS_RUNNER_HOOK_JOB_STARTED`, that fails any job whose event is not a dispatch or whose repository or actor is not on the box's own allow lists. `start` is the one command a box operator runs ([#937](https://github.com/Omega-JS-Stack/omega/issues/937)): it installs a bare box, refreshes a stale actions/runner download in place, and registers any admin org the box does not serve yet before bringing every org online
|
|
309
309
|
- [docs/test-framework.md](../docs/test-framework.md) — writing tests, running them, layers
|
|
310
310
|
- [docs/test-boot-layer.md](../docs/test-boot-layer.md) — the `boot` test layer: consumer end-to-end smoke + @omega.js/desktop's framework self-test from the repo via the bundled fixture (`src/test/fixtures/consumer-app/`) + `OMEGA_TEST_BOOT_PROJECT` (@omega.js/desktop's analog of @omega.js/backend/BXM/UJM `*_TEST_BOOT_PROJECT`)
|
|
311
311
|
- [docs/build-system.md](../docs/build-system.md) — gulp, esbuild, electron-builder pipeline
|
package/docs/releasing.md
CHANGED
|
@@ -92,7 +92,7 @@ Every verb runs `ensureTarget()` first ([#675](https://github.com/Omega-JS-Stack
|
|
|
92
92
|
`npx omega deploy` adds the network half as a precheck (`--no-secrets` opts out):
|
|
93
93
|
|
|
94
94
|
- Validates signing prereqs (warns if missing — non-fatal).
|
|
95
|
-
- Pushes the composed `.env` → GitHub Actions secrets over the `gh` CLI (`gh auth login`; a CI run, an empty cascade, no remote,
|
|
95
|
+
- Pushes the composed `.env` → GitHub Actions secrets over the `gh` CLI (`gh auth login`; a CI run, an empty cascade, or no GitHub remote skips loudly, and a checkout whose `origin` is not the derived source repo refuses on the one drift line, `origin is <slug> but config derives <derived>: fix repo.org in config/omega.json5 or move the repo`, [#934](https://github.com/Omega-JS-Stack/omega/issues/934)).
|
|
96
96
|
|
|
97
97
|
Now drop your cert files:
|
|
98
98
|
|
|
@@ -174,7 +174,9 @@ build needs setup; matrix over the resolved OSes: npm ci, then
|
|
|
174
174
|
`npm run release:local` on mac and linux (sign, notarize, and
|
|
175
175
|
electron-builder publishes the DRAFT release in the brand's releases
|
|
176
176
|
repo), and `npm run package` on windows, whose unsigned output uploads
|
|
177
|
-
as the `windows-unsigned` artifact
|
|
177
|
+
as the `windows-unsigned` artifact, a one-day intermediate that
|
|
178
|
+
windows-sign consumes in the same run (the releases repo is the
|
|
179
|
+
durable home)
|
|
178
180
|
windows-strategy needs [setup, build]; reads platforms.windows.signing.strategy from config
|
|
179
181
|
(only when windows is in the matrix)
|
|
180
182
|
windows-sign the self-hosted EV-token box, hosted windows-latest for the cloud
|
package/docs/runner.md
CHANGED
|
@@ -56,7 +56,7 @@ Then, in a normal (non-elevated) PowerShell:
|
|
|
56
56
|
|
|
57
57
|
Then the orgs: a checkbox of every org your token administers, in alphabetical order. Ticked by default are the orgs this box already answered for — the saved `OMEGA_RUNNER_ORGS`, else the orgs it actually registered, else NOTHING. A token that administers 35 orgs must never register 35 runners on one Enter. Ticking nothing is refused too — a runner registered against no org is not a runner. Off a TTY the walk asks nothing: `config` says so and stops (edit the file instead), `install` refuses only when a required key is missing, and a blank org list still means every org the token administers, because CI has no keyboard.
|
|
58
58
|
|
|
59
|
-
`start` is the exception, deliberately: it asks only for MISSING required keys, never the checkbox, and warns when the file's org list is not the one this install registered.
|
|
59
|
+
`start` is the exception, deliberately: it asks only for MISSING required keys, never the checkbox, and warns when the file's org list is not the one this install registered. On a bare box (nothing installed yet) `start` runs install itself, so there it is install's full walk, checkbox included.
|
|
60
60
|
|
|
61
61
|
Orgs in `OMEGA_RUNNER_ORGS` you do not administer are named in a warning and skipped.
|
|
62
62
|
|
|
@@ -64,13 +64,13 @@ Orgs in `OMEGA_RUNNER_ORGS` you do not administer are named in a warning and ski
|
|
|
64
64
|
|
|
65
65
|
```powershell
|
|
66
66
|
npx omega runner status # registered orgs, Startup shortcuts, live listeners, legacy leftovers
|
|
67
|
-
npx omega runner start #
|
|
67
|
+
npx omega runner start # the one command: install a bare box, else heal HOME and the job guard, refresh a stale actions/runner in place,
|
|
68
|
+
# register any admin org not served yet, then bring EVERY registered org up, detached (an org already alive is skipped)
|
|
68
69
|
npx omega runner restart # stop, wait for the listeners to go, then start
|
|
69
70
|
npx omega runner stop # kill every Runner.Listener.exe under the runner home
|
|
70
|
-
npx omega runner install #
|
|
71
|
+
npx omega runner install # the explicit clean rebuild: tears down first, so re-running is safe
|
|
71
72
|
npx omega runner config # the same full walk install runs — every key, current values as the defaults, plus the orgs
|
|
72
73
|
npx omega runner register-org <org># register one specific org
|
|
73
|
-
npx omega runner self-update # npm i -g @omega.js/desktop@latest
|
|
74
74
|
npx omega runner uninstall # remove everything, legacy services, tasks and the em-runner install included
|
|
75
75
|
npx omega runner monitor # tail the signing event log
|
|
76
76
|
```
|
|
@@ -79,13 +79,13 @@ Notes worth knowing before you use them:
|
|
|
79
79
|
|
|
80
80
|
- **`config` is `install`'s configuration step, alone.** Same walk, same defaults, same order — it just does not go on to register anything. A saved org your token no longer administers is named before the checkbox, since it cannot appear in it. Windows-only, and terminal-only (with nothing to ask with it stops and names the file). When the orgs you pick are not the ones this install registered, it says to re-run `install`.
|
|
81
81
|
- **Every subcommand tees its output to `<runner home>\logs\runner.log`**, and so does `npx omega sign-windows` — including inside a `windows-sign` job, which is exactly when the box's own record is wanted (the log lives in the runner home, never in a workspace, so the usual "no logs in CI" rule does not apply to it). `runner status` prints the path on its own line. It APPENDS, because more than one process writes it: `start` returns as soon as the runners are spawned, and every `sign-windows` those listeners go on to run adds to the same file. Each run stamps its own `# omega log` header; nothing rotates it, so delete the file when you want a clean one. `uninstall` keeps `.env` and `logs\`, since the log it is writing while it runs is the trail of that uninstall.
|
|
82
|
-
- **A refusal writes nothing.** Off Windows every subcommand but `
|
|
82
|
+
- **A refusal writes nothing.** Off Windows every subcommand but `monitor` stops at the platform check before the log file is opened, so running one on a Mac by accident leaves no `.gh-runners/` in the directory you were standing in.
|
|
83
83
|
- **The box verbs ignore the project's `.env` cascade.** `runner` and `sign-windows` read the shell and `<runner home>\.env`, nothing else, so running them from inside a brand folder can never hand a brand's `GH_TOKEN` to the box ([#337](https://github.com/Omega-JS-Stack/omega/issues/337)). Every other verb keeps the cascade.
|
|
84
|
-
- **`start` brings up EVERY registered org, detached.** It walks the Startup shortcuts in order and spawns one hidden runner per org, so the terminal comes straight back and nothing is lost by not watching it: the listener's output is the JSONL log `monitor` tails. Running it again is safe, which is the point of it: an org whose listener is already alive is named with its PID and skipped, never duplicated and never a refusal. Exit 0 when every org ends up online, 1 when a spawn failed. A listener sitting in session 0 is skipped too, but loudly: it is alive and will fail every job it picks up, so the line names the session and points at `restart`. Killing a listener is `stop`'s job, never a start's.
|
|
84
|
+
- **`start` brings up EVERY registered org, detached.** It walks the Startup shortcuts in order and spawns one hidden runner per org, so the terminal comes straight back and nothing is lost by not watching it: the listener's output is the JSONL log `monitor` tails. Running it again is safe, which is the point of it: an org whose listener is already alive is named with its PID and skipped, never duplicated and never a refusal. Exit 0 when every org ends up online, 1 when a spawn or a registration failed. A listener sitting in session 0 is skipped too, but loudly: it is alive and will fail every job it picks up, so the line names the session and points at `restart`. Killing a listener is `stop`'s job, never a start's.
|
|
85
85
|
- **`restart` is `stop` then `start`**, in that order, for the loop you would otherwise run by hand after a config change. It settles two things the two commands typed in sequence do not: the box config walk runs FIRST, so a box missing a required key is refused before anything is killed rather than left stopped, and each org's dir is polled (5 seconds) until no listener stands in it, because `taskkill` returns before the process is gone and a start that raced it would read the dying listener as "already running". A listener still standing after the wait is reported and that org is left alone, exit 1.
|
|
86
86
|
- **`stop` leaves the Startup shortcuts in place**, so a logout/login brings the runner back. For a permanent stop, run `uninstall`.
|
|
87
|
-
- **Every subcommand except `
|
|
88
|
-
- **A test process cannot touch a real box.** Every subcommand that changes the machine
|
|
87
|
+
- **Every subcommand except `monitor` refuses on non-Windows.** `OMEGA_RUNNER_FORCE=1` overrides it, for framework tests only.
|
|
88
|
+
- **A test process cannot touch a real box.** Every subcommand that changes the machine (`install`, `config`, `register-org`, `start`, `restart`, `stop`, `uninstall`) refuses, before the platform check and naming `OMEGA_TEST_RUNNER`, whenever the run is a test (`omega test` sets that variable; `OMEGA_TEST_MODE` counts too, the marker a sibling framework's runner or a consumer's own test script sets) and a home it could act on is not a scratch one, under a `.temp` directory or the OS temp dir. BOTH homes are checked, the passed one and the module-level `RUNNER_HOME`, because the box's `.env` was already read into the process from the latter when the command module was required; the refusal names whichever is real. The marker is read from the process environment only — an injected environment is a fixture for the config walk, never an answer to "am I a test". The Startup folder is the THIRD surface checked, because no home scopes it — `uninstall` sweeps every `omega-runner-*.cmd` in the folder whatever home it was given, and a case whose two homes were both scratch deleted the box's three real shortcuts. It has its own scratch seam, `OMEGA_RUNNER_STARTUP_DIR`, and a test run against the real folder refuses naming both. The surfaces that no path can redirect at all — the watcher service, the legacy logon tasks, the `actions.runner.*` services, and the legacy `em-runner` homes — are simply not swept under a test run; `uninstall` says so on one line instead. A test run also never kills a process whose ExecutablePath it cannot read: that path belongs to another account, and matching it scopes to no home at all, so `stop` and `uninstall` drop the clause instead of `taskkill /F`-ing a stranger's runner. And it never deregisters on its own: `deregisterOrgRunners` mints a real removal token and runs each org directory's `config.cmd`, neither of them home-scoped, so with a roster to remove a test run refuses, naming the seam, unless `exec` is injected and, while `GH_TOKEN` is set, `getRemoveToken` too. `start` and `restart` skip their org check the same way, on one line, unless both `_discoverOrgs` and `_registerOrg` are injected: registering acts on GitHub, never on a home. A framework suite that wants to drive these points `OMEGA_RUNNER_HOME` and `OMEGA_RUNNER_STARTUP_DIR` at scratches *before* the command module is required, since both are resolved once, at require time. `sign-windows` follows the same rule for its log: from a test run pointed at a real home it tees nowhere rather than write the box's own record. This exists because a suite once ran a real `install` on the box and registered 34 orgs.
|
|
89
89
|
- **`uninstall` deregisters on the GitHub side first, and keeps whatever did not come off.** It walks every `actions-runner-<org>\` directory under the runner home and runs that directory's own `config.cmd remove` before anything is deleted. If a removal exits non-zero — or `GH_TOKEN` is not set, so no removal token can be minted — that runner is still registered, so its directory SURVIVES the uninstall and the summary names the org. Re-running `uninstall` retries it. Belt and braces: `register-org` also deletes every org-side runner starting with this host's prefix before it registers a new one.
|
|
90
90
|
|
|
91
91
|
## Upgrading from the electron-manager runner
|
|
@@ -102,7 +102,9 @@ So the upgrade on the box is the ordinary one: `npx omega runner install` with `
|
|
|
102
102
|
|
|
103
103
|
## Adding a new org
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
No process watches for new orgs, but `start` checks every time it runs: `npx omega runner start` lists the orgs your `GH_TOKEN` administers, narrows them by `OMEGA_RUNNER_ORGS` when it is set, registers each one this box does not serve yet (Startup shortcut included), and brings it online with the rest. Orgs registered outside the list stay registered; `npx omega runner install` is the command that reshapes the box to the list exactly. An org check that cannot reach GitHub is one warn line, and `start` carries on and brings every registered org online.
|
|
106
|
+
|
|
107
|
+
To register one specific org alone:
|
|
106
108
|
|
|
107
109
|
```powershell
|
|
108
110
|
npx omega runner register-org <org-name>
|
|
@@ -280,7 +282,7 @@ The SafeNet driver sometimes detaches after a major OS update. Open the SafeNet
|
|
|
280
282
|
|
|
281
283
|
## Upgrading the actions/runner binary
|
|
282
284
|
|
|
283
|
-
`ACTIONS_RUNNER_VERSION` in `src/commands/runner.js` pins it. Bump the constant, ship
|
|
285
|
+
`ACTIONS_RUNNER_VERSION` in `src/commands/runner.js` pins it. Bump the constant, ship, then on the box run `npx omega runner start`. It sees that the version `config.json` recorded is not the pinned one, re-downloads `_template` in place, and copies it over each org dir whose listener is not running. `.runner`, `.credentials` and `.env` are not in the template, so every registration survives, and nothing is torn down. An org whose listener IS running has its binaries locked: it is skipped with one line naming `npx omega runner restart`, and the new version is recorded only once every org dir took it, so a `restart` (which stops first) finishes the job. `npx omega runner install` remains the clean rebuild when you want one.
|
|
284
286
|
|
|
285
287
|
## Related
|
|
286
288
|
|
package/docs/shared/brands.md
CHANGED
|
@@ -16,7 +16,7 @@ Fixture for the AUTOMATED corpus/e2e suites: offline, `demo-*` Firebase, determi
|
|
|
16
16
|
|
|
17
17
|
### `brands/playground-omega` — "OMEGA Playground", the standing live test brand
|
|
18
18
|
|
|
19
|
-
Renamed from omega-brand (Ian 2026-07-11, zero ambiguity). Born through the real wizard: id `playground` (shortened on 2026-09-10, [#808](https://github.com/Omega-JS-Stack/omega/issues/808), so the `<brand.id>-<role>` rule derives clean repo names off it), url playground.omegajs.dev, a SUBDOMAIN so derived surfaces never claim the real omegajs.dev. Its THREE repos all derive from that id, none of them typed ([#883](https://github.com/Omega-JS-Stack/omega/issues/883), all under the one `repo: { provider: 'github', org: 'Omega-JS-Stack' }` block): `playground-omega` (the
|
|
19
|
+
Renamed from omega-brand (Ian 2026-07-11, zero ambiguity). Born through the real wizard: id `playground` (shortened on 2026-09-10, [#808](https://github.com/Omega-JS-Stack/omega/issues/808), so the `<brand.id>-<role>` rule derives clean repo names off it), url playground.omegajs.dev, a SUBDOMAIN so derived surfaces never claim the real omegajs.dev. Its THREE repos all derive from that id, none of them typed ([#883](https://github.com/Omega-JS-Stack/omega/issues/883), all under the one `repo: { provider: 'github', org: 'Omega-JS-Stack' }` block): `playground-omega` (the PUBLIC source repo since 2026-09-24, so its Actions runs never count against the org storage allowance; the folder name here, whose `main` every `omega deploy` snapshots this folder onto, [docs/shared/deploys.md](deploys.md)), `playground-releases` (public: the desktop installers, the extension zips and the autoupdater feed), and `playground-web` (public: the BUILT site alone, one force-orphan commit on `gh-pages`, served by Pages at playground.omegajs.dev, which is what let the source repo stay private on a free org before it went public). The third, the private `playground-rehearsal` snapshot, retired with the rehearsal itself ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)). Points at the real-but-throwaway Firebase project `omegajs-playground` (ITW-org-owned since 2026-07-11; sanctioned for live proofs: Blaze it, break it, delete it; it is TEST INFRASTRUCTURE, never production).
|
|
20
20
|
|
|
21
21
|
REBRANDED **OMEGA Playground** (Ian 2026-08-21, [#433](https://github.com/Omega-JS-Stack/omega/issues/433) — REVERSES the 2026-07-19 fiction ruling, whose invented brand read as a real product in the ad and analytics consoles). The doctrine now: the playground is openly the OMEGA test surface — "we can mess around here, nothing live actually matters" — so drift from the real omegajs.dev site reads as a demo doing its job, never as a broken copy. Infra identity unchanged (id, url, green accent, classy theme); name and voice are the only things that moved, and the catalog's display names stay placeholder demo copy. Cloud-side display names (the Meta pixel, the GA property + streams, the GCP project name) are the manager's follow-up.
|
|
22
22
|
|
package/docs/shared/config.md
CHANGED
|
@@ -1433,6 +1433,13 @@ of any kind exists: a repo name that must differ is a brand id that must differ.
|
|
|
1433
1433
|
| Releases | `releasesRepo(config)` → `<repo.org>/<brand.id>-releases` | always public (the desktop updater polls it with no token) | every target's built artifacts: desktop installers, the extension's zips, tagged per target |
|
|
1434
1434
|
| Website, one per GitHub-hosted web target | `websiteRepo(config, name)` → `<repo.org>/<brand.id>-<target name>` | private only when the brand is private AND the org's plan allows Pages from a private repo, else public, and the manage walk says which | the BUILT site only, one force-orphan commit on `gh-pages`, served by Pages at the target's url |
|
|
1435
1435
|
|
|
1436
|
+
**`repoDrift(originSlug, config)`** is the one comparison of a checkout's `origin` with
|
|
1437
|
+
the source repo ([#934](https://github.com/Omega-JS-Stack/omega/issues/934)): the whole
|
|
1438
|
+
slug, case-insensitive, null when they agree or the config derives no source repo, else
|
|
1439
|
+
the line `origin is <slug> but config derives <derived>: fix repo.org in
|
|
1440
|
+
config/omega.json5 or move the repo`. The boot prelude prints it; `omega manage` and
|
|
1441
|
+
`omega deploy` refuse with it ([deploys.md](deploys.md#the-origin-gate-934)).
|
|
1442
|
+
|
|
1436
1443
|
**Visibility lives in the brand root's `package.json`**, never in omega.json5
|
|
1437
1444
|
(`brandVisibility(brandRoot)`): `private: true` or the field ABSENT is a private brand
|
|
1438
1445
|
(every brand monorepo is private by default, Ian 2026-09-11), and only a literal `false`
|