@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 CHANGED
@@ -152,7 +152,7 @@ Each subsystem has its own API reference under [`docs/`](docs/):
152
152
  - [installer-options](docs/installer-options.md) — installer/distribution config: NSIS one-click defaults, ia32 inclusion, app.category mapping, `{YEAR}` copyright token, snap publishing (default-on with cred-gated auto-skip), MAS roadmap
153
153
  - [signing](docs/signing.md) — macOS + Windows code signing reference, cert files, env vars
154
154
  - [releasing](docs/releasing.md) — end-to-end release walkthrough (`.env` → GitHub Release)
155
- - [runner](docs/runner.md) — Windows EV-token signing runner — `npx omega runner install`, auto-onboards new GH orgs, `npx omega runner monitor` for a live signing event tail
155
+ - [runner](docs/runner.md) — Windows EV-token signing runner — `npx omega runner start`, which onboards new GH orgs, `npx omega runner monitor` for a live signing event tail
156
156
  - [test-framework](docs/test-framework.md) — writing tests, running them, layers
157
157
  - [test-boot-layer](docs/test-boot-layer.md) — boot test layer (spawns the consumer's actual built bundle for end-to-end smoke tests)
158
158
  - [build-system](docs/build-system.md) — gulp, esbuild, electron-builder pipeline
@@ -14,18 +14,20 @@
14
14
  // the defaults — and the orgs it serves. How a value that
15
15
  // is already there gets changed.
16
16
  // register-org <org> Register the runner against one specific GH org.
17
- // start Bring EVERY registered org's runner online, detached,
18
- // in Startup-shortcut order. An org already alive is
19
- // named and skipped; the terminal is never taken over.
17
+ // start The one command: installs a bare box, refreshes a stale
18
+ // actions/runner download in place, registers any admin
19
+ // org the box does not serve yet, then brings EVERY
20
+ // registered org online, detached, in Startup-shortcut
21
+ // order. An org already alive is named and skipped; the
22
+ // terminal is never taken over.
20
23
  // restart stop, wait for every listener to go, then start.
21
24
  // stop Kill every Runner.Listener.exe under RUNNER_HOME.
22
25
  // status Registered orgs, Startup shortcuts, live listeners.
23
26
  // uninstall Remove everything — legacy services and tasks, and the
24
27
  // electron-manager era's em-runner install, included.
25
- // self-update Force an immediate self-update of @omega.js/desktop (npm i -g @omega.js/desktop@latest).
26
28
  // monitor Tail the JSONL signing event log, pretty-printed.
27
29
  //
28
- // Every subcommand except `self-update` and `monitor` refuses on non-Windows
30
+ // Every subcommand except `monitor` refuses on non-Windows
29
31
  // (set OMEGA_RUNNER_FORCE=1 to override — testing only).
30
32
 
31
33
  const path = require('path');
@@ -35,7 +37,6 @@ const jetpack = require('fs-jetpack');
35
37
 
36
38
  const Manager = new (require('../build.js'));
37
39
  const logger = Manager.logger('runner');
38
- const { safeInstall } = require('../utils/safe-install');
39
40
 
40
41
  const RUNNER_LABELS = ['self-hosted', 'windows', 'ev-token'];
41
42
 
@@ -103,8 +104,8 @@ function orgRunnerDir(org, home) {
103
104
  }
104
105
  const ACTIONS_RUNNER_VERSION = '2.319.1'; // pinned; bump intentionally
105
106
 
106
- // The subcommands, and the two of them that run on any platform: self-update is
107
- // an npm install, monitor only reads a log file. Every other one is the box's.
107
+ // The subcommands, and the one of them that runs on any platform: monitor only
108
+ // reads a log file. Every other one is the box's.
108
109
  const SUBCOMMANDS = {
109
110
  'install': install,
110
111
  'config': configureRunner,
@@ -114,17 +115,16 @@ const SUBCOMMANDS = {
114
115
  'stop': stopServices,
115
116
  'status': statusServices,
116
117
  'uninstall': uninstall,
117
- 'self-update': selfUpdate,
118
118
  'monitor': monitor,
119
119
  };
120
- const ANY_PLATFORM_SUBCOMMANDS = new Set(['self-update', 'monitor']);
120
+ const ANY_PLATFORM_SUBCOMMANDS = new Set(['monitor']);
121
121
 
122
122
  // The ones that CHANGE the machine: they register, deregister, download, delete,
123
- // kill listeners, rewrite the box's own configuration, or run a global npm
124
- // install. A test process may only point these at a scratch home
123
+ // kill listeners, or rewrite the box's own configuration. A test process may
124
+ // only point these at a scratch home
125
125
  // ([#337](https://github.com/Omega-JS-Stack/omega/issues/337) — the suite once
126
126
  // ran a real install against the box). `status` and `monitor` only read.
127
- const MUTATING_SUBCOMMANDS = new Set(['install', 'config', 'register-org', 'start', 'restart', 'stop', 'uninstall', 'self-update']);
127
+ const MUTATING_SUBCOMMANDS = new Set(['install', 'config', 'register-org', 'start', 'restart', 'stop', 'uninstall']);
128
128
 
129
129
  module.exports = async function (options) {
130
130
  options = options || {};
@@ -261,7 +261,7 @@ async function install(options) {
261
261
  // admin orgs on a tick ran as NT AUTHORITY\NETWORK SERVICE in Session 0, so
262
262
  // the runners it spawned could not see the user's cert store and the Startup
263
263
  // shortcuts it wrote landed in the wrong profile. Adding a new org is a
264
- // `mgr runner install` away; uninstall() still tears down any leftover
264
+ // `npx omega runner start` away; uninstall() still tears down any leftover
265
265
  // watcher service from an older install.
266
266
 
267
267
  // 5. Save install metadata.
@@ -304,7 +304,7 @@ async function install(options) {
304
304
  logger.log('');
305
305
  logger.log(`Bringing every registered runner (${succeeded.length}) online in the background. 'npx omega runner status' shows them, 'npx omega runner monitor' tails the log.`);
306
306
  logger.log('');
307
- await startServices({ ...options, _home: home });
307
+ await startServices({ ...options, _home: home, _reconcileDone: true });
308
308
  }
309
309
  }
310
310
 
@@ -416,7 +416,7 @@ async function registerOrg(options) {
416
416
  const runnerDir = orgRunnerDir(org, home);
417
417
  const templateDir = options._templateDir || path.join(home, '_template');
418
418
  if (!jetpack.exists(path.join(templateDir, 'config.cmd'))) {
419
- throw new Error(`actions/runner template not found at ${templateDir}. Run 'npx omega runner install' first.`);
419
+ throw new Error(`actions/runner template not found at ${templateDir}. Run 'npx omega runner start' first.`);
420
420
  }
421
421
  if (jetpack.exists(runnerDir)) {
422
422
  logger.log(` Removing stale actions-runner-${org.toLowerCase()}/ before re-clone…`);
@@ -528,7 +528,8 @@ async function registerOrg(options) {
528
528
  // What `start` settles before it touches a listener: the box's required keys —
529
529
  // asked in a terminal and refused without one, exactly like install — and then
530
530
  // the orgs the .env names against the orgs this install actually registered.
531
- // start registers nothing; it names the command that would.
531
+ // start only ever ADDS a named admin org (reconcileRunnerInstall); reshaping the
532
+ // box to the list is install's, so the mismatch names it.
532
533
  async function startPreflight(options) {
533
534
  const { home, env = process.env, logger: log = logger, prompt, interactive } = options || {};
534
535
  await ensureRunnerConfig({ home, env, logger: log, prompt, interactive });
@@ -557,7 +558,7 @@ async function startPreflight(options) {
557
558
  const named = parseRunnerOrgs(env.OMEGA_RUNNER_ORGS);
558
559
  const registered = readConfig(home).registeredOrgs || [];
559
560
  if (named.length > 0 && !sameRunnerOrgs(named, registered)) {
560
- log.warn(`OMEGA_RUNNER_ORGS names ${named.join(', ')} but this install registered ${registered.join(', ') || '(none)'}. Run \`npx omega runner install\` to apply.`);
561
+ log.warn(`OMEGA_RUNNER_ORGS names ${named.join(', ')} but this install registered ${registered.join(', ') || '(none)'}. \`start\` registers a named admin org it does not serve yet and keeps the rest; \`npx omega runner install\` reshapes the box to the list.`);
561
562
  }
562
563
  }
563
564
 
@@ -574,6 +575,92 @@ function runnerStartupTargets(home) {
574
575
  });
575
576
  }
576
577
 
578
+ // What `start` settles between the preflight and the spawns
579
+ // ([#937](https://github.com/Omega-JS-Stack/omega/issues/937)), so a box never
580
+ // needs a second verb to catch up:
581
+ // 1. a stale actions/runner download is refreshed IN PLACE: the template is
582
+ // re-downloaded and laid over each org dir whose listener is down. The
583
+ // registration (`.runner`, `.credentials`, `.env`) is never in the
584
+ // template, so it survives the copy. A live listener holds its binaries
585
+ // open, so that org is skipped and named with `restart`, and the new
586
+ // version is recorded only once every org dir took it. No teardown. A
587
+ // download or a copy that fails is one warn and a skipped refresh, never
588
+ // a failed start: the next start tries again.
589
+ // 2. an admin org the box does not serve yet (OMEGA_RUNNER_ORGS honoured) is
590
+ // registered, Startup shortcut and all. The only step that needs GitHub,
591
+ // so discovery that fails is one line and the box still comes online.
592
+ // GH_TOKEN is never missing here: it is a required key, and the preflight
593
+ // refuses a box without one. Orgs registered outside the filter stay;
594
+ // reshaping the box to the list is install's.
595
+ //
596
+ // `_download`, `_discoverOrgs` and `_registerOrg` are the tests' seams, the way
597
+ // `_spawn` is. Registration is not home-scoped (it acts on GitHub with the real
598
+ // token, whatever scratch home a case made), so a test run that has not
599
+ // injected both GitHub seams skips the org check instead of reaching it.
600
+ async function reconcileRunnerInstall(options) {
601
+ const home = options._home;
602
+ const env = options._env;
603
+ const log = options._logger;
604
+ const listen = options._listeners;
605
+ const download = options._download || downloadActionsRunner;
606
+ const templateDir = path.join(home, '_template');
607
+
608
+ const installed = readConfig(home).actionsRunnerVersion;
609
+ if (installed !== ACTIONS_RUNNER_VERSION) {
610
+ log.log(`actions/runner ${installed || '(unrecorded)'} is installed and ${ACTIONS_RUNNER_VERSION} is pinned: refreshing in place.`);
611
+ let downloaded = true;
612
+ try {
613
+ await download(templateDir, home);
614
+ } catch (e) {
615
+ log.warn(`✗ actions/runner ${ACTIONS_RUNNER_VERSION} download failed (${e.message}): refresh skipped until the next start.`);
616
+ downloaded = false;
617
+ }
618
+ if (downloaded) {
619
+ let skipped = 0;
620
+ for (const { org, dir } of listOrgRunnerDirs(home)) {
621
+ if (listen(dir).length > 0) {
622
+ log.warn(`· ${org} is running, so its actions/runner files are locked and were not refreshed. Run \`npx omega runner restart\` to refresh it.`);
623
+ skipped++;
624
+ continue;
625
+ }
626
+ try {
627
+ jetpack.copy(templateDir, dir, { overwrite: true });
628
+ log.log(`✓ ${org} refreshed to actions/runner ${ACTIONS_RUNNER_VERSION}`);
629
+ } catch (e) {
630
+ log.warn(`✗ ${org} was not refreshed (${e.message}): the next start tries again.`);
631
+ skipped++;
632
+ }
633
+ }
634
+ if (skipped === 0) saveConfig({ actionsRunnerVersion: ACTIONS_RUNNER_VERSION }, home);
635
+ }
636
+ }
637
+
638
+ if (isTestRun() && !(options._discoverOrgs && options._registerOrg)) {
639
+ log.log('Org check skipped: a test run never asks GitHub unless both _discoverOrgs and _registerOrg are injected.');
640
+ return;
641
+ }
642
+ const discover = options._discoverOrgs || discoverAdminOrgs;
643
+ const register = options._registerOrg || registerOrg;
644
+ let adminOrgs;
645
+ try {
646
+ adminOrgs = await discover();
647
+ } catch (e) {
648
+ log.warn(`Org check skipped: could not list your admin orgs (${e.message}).`);
649
+ return;
650
+ }
651
+ const { orgs } = selectRunnerOrgs(adminOrgs, env.OMEGA_RUNNER_ORGS);
652
+ const served = new Set((readConfig(home).registeredOrgs || []).map((org) => org.toLowerCase()));
653
+ for (const org of orgs.filter((o) => !served.has(o.toLowerCase()))) {
654
+ log.log(`Registering ${org}: an admin org this box does not serve yet.`);
655
+ try {
656
+ await register({ ...options, _: ['runner', 'register-org', org], _templateDir: templateDir });
657
+ } catch (e) {
658
+ log.warn(`✗ register ${org}: ${e.message}`);
659
+ process.exitCode = 1;
660
+ }
661
+ }
662
+ }
663
+
577
664
  // Every registered org, brought online detached, in Startup-shortcut order.
578
665
  //
579
666
  // A box registers one runner per org, and `start` used to bring up exactly ONE
@@ -589,7 +676,15 @@ function runnerStartupTargets(home) {
589
676
  // `_spawn` and `_listeners` are injected by the tests, the same way `_home` and
590
677
  // `_logger` are: neither cmd.exe nor the process scan exists off Windows.
591
678
  // `_preflightDone` is `restart`'s: it settles the box config BEFORE it kills
592
- // anything, so the walk must not run a second time here.
679
+ // anything, so the walk must not run a second time here. The reconcile is NOT
680
+ // gated on it: after restart's stop is exactly when nothing is locked.
681
+ // `_reconcileDone` is install's alone: it just downloaded and registered, so the
682
+ // reconcile is skipped there.
683
+ //
684
+ // A bare box (no template to clone from) is handed to install, `_install` in
685
+ // the tests, which ends by calling this function again with the template in
686
+ // place. A test run without `_install` refuses: the real install downloads,
687
+ // discovers and registers against GitHub, and no scratch home scopes that.
593
688
  async function startServices(options) {
594
689
  options = options || {};
595
690
  ensureWindows();
@@ -597,8 +692,23 @@ async function startServices(options) {
597
692
  const log = options._logger || logger;
598
693
  const spawnIt = options._spawn || spawnRunnerDetached;
599
694
  const listen = options._listeners || listRunnerListenerProcessesUnder;
695
+ if (!jetpack.exists(path.join(home, '_template', 'config.cmd'))) {
696
+ if (isTestRun() && !options._install) {
697
+ throw new Error(
698
+ `Refusing to install onto the bare runner home ${home}: this is a test run `
699
+ + '(OMEGA_TEST_RUNNER / OMEGA_TEST_MODE) and the call did not inject `_install`. The real install '
700
+ + 'downloads actions/runner and registers every admin org against GitHub with GH_TOKEN.',
701
+ );
702
+ }
703
+ log.log(`No runner install under ${home}: running the full install.`);
704
+ return (options._install || install)(options);
705
+ }
706
+ const env = options._env || process.env;
600
707
  if (!options._preflightDone) {
601
- await startPreflight({ home, env: options._env || process.env, logger: log, prompt: options._prompt, interactive: options._interactive });
708
+ await startPreflight({ home, env, logger: log, prompt: options._prompt, interactive: options._interactive });
709
+ }
710
+ if (!options._reconcileDone) {
711
+ await reconcileRunnerInstall({ ...options, _home: home, _env: env, _logger: log, _listeners: listen });
602
712
  }
603
713
  const targets = runnerStartupTargets(home);
604
714
  if (targets.length === 0) {
@@ -762,7 +872,7 @@ async function statusServices(options) {
762
872
  logger.log('');
763
873
  const report = orgRunnerReport({ home });
764
874
  if (report.length === 0) {
765
- logger.warn('No orgs registered. Run `npx omega runner install`.');
875
+ logger.warn('No orgs registered. Run `npx omega runner start`.');
766
876
  } else {
767
877
  logger.log(`Per-org runner state (${report.length}):`);
768
878
  for (const { state, lines } of report) {
@@ -1161,19 +1271,6 @@ function identifyHandleHolders(targetPath) {
1161
1271
  }
1162
1272
  }
1163
1273
 
1164
- // ─── self-update ────────────────────────────────────────────────────────────────
1165
- async function selfUpdate() {
1166
- const { execute } = require('node-powertools');
1167
- logger.log('Updating @omega.js/desktop to latest…');
1168
- try {
1169
- const out = await safeInstall('npm i -g @omega.js/desktop@latest');
1170
- logger.log(out);
1171
- logger.log('✓ @omega.js/desktop updated.');
1172
- } catch (e) {
1173
- logger.warn(`Self-update failed: ${e.message}`);
1174
- }
1175
- }
1176
-
1177
1274
  // ─── helpers ────────────────────────────────────────────────────────────────────
1178
1275
 
1179
1276
  const WATCHER_SERVICE_NAME = 'omega-runner-watcher';
@@ -1571,8 +1668,8 @@ function listLegacyRunnerStartupShortcuts(startupDir) {
1571
1668
  // wrapper that blocks for the lifetime of the listener with cwd = runnerDir;
1572
1669
  // a long-lived cmd.exe holding cwd inside RUNNER_HOME blocks every later
1573
1670
  // uninstall with EPERM. Bypassing run.cmd loses its self-update relaunch
1574
- // path, but `mgr runner install` refreshing the runner binary is the
1575
- // preferred update mechanism in @omega.js/desktop anyway.
1671
+ // path, but `npx omega runner start` refreshing the runner binary in place
1672
+ // is the update mechanism in @omega.js/desktop anyway.
1576
1673
  //
1577
1674
  // Detached + windowsHide + ignored stdio = no console window flashes during
1578
1675
  // install and the listener survives the install command's exit. UAC-
@@ -1733,7 +1830,7 @@ async function monitor(options) {
1733
1830
  // exactly which orgs the monitor will pick up signing events from.
1734
1831
  const report = orgRunnerReport({ home });
1735
1832
  if (report.length === 0) {
1736
- logger.log('(no orgs registered yet — run `npx omega runner install` first)');
1833
+ logger.log('(no orgs registered yet: run `npx omega runner start` first)');
1737
1834
  } else {
1738
1835
  logger.log(`Monitoring signing requests across ${report.length} org(s):`);
1739
1836
  for (const { state, lines } of report) {
@@ -239,6 +239,8 @@ jobs:
239
239
  uses: actions/upload-artifact@v7
240
240
  with:
241
241
  name: windows-unsigned
242
+ # An intermediate windows-sign consumes in this same run; the releases repo is the durable home.
243
+ retention-days: 1
242
244
  # Capture installer + auto-updater feed + blockmap so windows-sign can re-upload them all.
243
245
  path: |
244
246
  release/*.exe
@@ -72,7 +72,7 @@ function checkJob(env, lists) {
72
72
  return { allowed: false, line: `Refusing this job: GITHUB_EVENT_NAME is "${event}", and this runner serves ${DISPATCH_EVENTS.join(' and ')} only.` };
73
73
  }
74
74
  if (repos.length === 0) {
75
- return { allowed: false, line: `Refusing this job: ${reposFile} lists no repository, so this runner allows none. Run \`npx omega runner install\` on the box, or add one \`owner/name\` per line.` };
75
+ return { allowed: false, line: `Refusing this job: ${reposFile} lists no repository, so this runner allows none. Run \`npx omega runner start\` on the box, or add one \`owner/name\` per line.` };
76
76
  }
77
77
  if (!repos.some((entry) => repoMatches(repository, entry))) {
78
78
  return { allowed: false, line: `Refusing this job: GITHUB_REPOSITORY is "${repository}", which is not listed in ${reposFile}.` };