@omega.js/desktop 0.50.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.
Files changed (268) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/css/tokens/_index.scss +1 -1
  3. package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +4 -1
  4. package/dist/assets/themes/base/_includes/frontend/sections/footer.html +13 -6
  5. package/dist/assets/themes/base/_includes/frontend/sections/nav.html +10 -8
  6. package/dist/assets/themes/base/_includes/global/sections/account.html +3 -1
  7. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +12 -8
  8. package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +10 -6
  9. package/dist/assets/themes/base/_includes/global/sections/page-header.html +8 -4
  10. package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +24 -24
  11. package/dist/assets/themes/base/_layouts/frontend/pages/about.html +10 -10
  12. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +35 -35
  13. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +4 -4
  14. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +1 -1
  15. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +4 -4
  16. package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +3 -3
  17. package/dist/assets/themes/base/_layouts/frontend/pages/download.html +34 -33
  18. package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +11 -11
  19. package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +1 -1
  20. package/dist/assets/themes/base/_layouts/frontend/pages/status.html +1 -1
  21. package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +9 -7
  22. package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +5 -3
  23. package/dist/assets/themes/base/_sections/about/letter/section.html +1 -1
  24. package/dist/assets/themes/base/_sections/about/letter/section.json5 +4 -4
  25. package/dist/assets/themes/base/_sections/marketing/bento/section.html +1 -1
  26. package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +15 -15
  27. package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +1 -1
  28. package/dist/assets/themes/base/_sections/marketing/hero/section.html +6 -6
  29. package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +10 -10
  30. package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +1 -1
  31. package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +2 -2
  32. package/dist/assets/themes/base/_sections/marketing/stats/section.html +1 -1
  33. package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +5 -5
  34. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +1 -1
  35. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +2 -2
  36. package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +10 -10
  37. package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +10 -10
  38. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +2 -2
  39. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +4 -4
  40. package/dist/build.js +69 -28
  41. package/dist/cli-run.js +20 -13
  42. package/dist/cli.js +12 -7
  43. package/dist/commands/build.js +0 -2
  44. package/dist/commands/deploy.js +116 -44
  45. package/dist/commands/finalize-release.js +5 -4
  46. package/dist/commands/launch.js +12 -7
  47. package/dist/commands/lib/deploy-precheck.js +66 -34
  48. package/dist/commands/lib/ensure-target.js +71 -11
  49. package/dist/commands/package.js +0 -1
  50. package/dist/commands/publish.js +11 -4
  51. package/dist/commands/release.js +99 -252
  52. package/dist/commands/runner.js +176 -42
  53. package/dist/commands/sign-windows.js +46 -15
  54. package/dist/commands/test.js +4 -3
  55. package/dist/commands/validate-certs.js +291 -115
  56. package/dist/config/page-template.html +4 -0
  57. package/dist/defaults/.github/workflows/build.yml +146 -64
  58. package/dist/defaults/AGENTS.md +3 -2
  59. package/dist/defaults/_.gitignore +4 -2
  60. package/dist/defaults/config/certs/README.md +25 -45
  61. package/dist/defaults/config/omega.json5 +66 -32
  62. package/dist/defaults/hooks/deploy/pre.js +10 -0
  63. package/dist/defaults/src/assets/scss/main.scss +12 -2
  64. package/dist/defaults/src/integrations/tray/index.js +1 -1
  65. package/dist/gulp/main.js +13 -25
  66. package/dist/gulp/tasks/audit.js +18 -6
  67. package/dist/gulp/tasks/build-config.js +181 -60
  68. package/dist/gulp/tasks/bundle.js +108 -36
  69. package/dist/gulp/tasks/release.js +86 -4
  70. package/dist/gulp/tasks/sass.js +11 -0
  71. package/dist/hooks/lib/notarize-tools.js +137 -0
  72. package/dist/hooks/notarize-artifacts.js +57 -0
  73. package/dist/hooks/notarize.js +59 -8
  74. package/dist/lib/auth-persistence.js +25 -0
  75. package/dist/lib/client-bridge.js +11 -8
  76. package/dist/lib/deep-link.js +15 -8
  77. package/dist/lib/protocol.js +7 -1
  78. package/dist/lib/restart-manager/install.js +6 -3
  79. package/dist/lib/sign-helpers/auto-unlock.js +65 -27
  80. package/dist/lib/sign-helpers/console-lock.js +34 -0
  81. package/dist/lib/sign-helpers/exec-with-limit.js +68 -0
  82. package/dist/lib/sign-helpers/resolve-icons.js +6 -4
  83. package/dist/lib/tray.js +7 -6
  84. package/dist/main.js +16 -0
  85. package/dist/preload.js +8 -0
  86. package/dist/renderer.js +16 -4
  87. package/dist/runner/job-started.js +104 -0
  88. package/dist/test/fixtures/consumer-app/config/omega.json5 +7 -0
  89. package/dist/test/fixtures/consumer-app/package.json +1 -1
  90. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +7 -1
  91. package/dist/test/harness/main-entry.js +2 -1
  92. package/dist/test/harness/renderer-preload.js +12 -3
  93. package/dist/test/runners/boot.js +101 -11
  94. package/dist/test/runners/electron.js +1 -1
  95. package/dist/test/suites/boot/consumer-app-boots.test.js +54 -0
  96. package/dist/test/suites/build/audit.test.js +45 -6
  97. package/dist/test/suites/build/auth-persistence-resolve.test.js +119 -0
  98. package/dist/test/suites/build/auto-unlock.test.js +105 -0
  99. package/dist/test/suites/build/boot-runner-timeout.test.js +253 -0
  100. package/dist/test/suites/build/brand-scss.test.js +106 -0
  101. package/dist/test/suites/build/build-config.test.js +146 -36
  102. package/dist/test/suites/build/build-json-bake.test.js +245 -0
  103. package/dist/test/suites/build/build-verbs.test.js +48 -15
  104. package/dist/test/suites/build/build-workflow-jobs.test.js +190 -0
  105. package/dist/test/suites/build/cli.test.js +32 -10
  106. package/dist/test/suites/build/config-schema.test.js +4 -4
  107. package/dist/test/suites/build/console-lock.test.js +51 -0
  108. package/dist/test/suites/build/defaults-scaffold.test.js +71 -2
  109. package/dist/test/suites/build/deploy-direct.test.js +241 -0
  110. package/dist/test/suites/build/deploy-dispatch.test.js +287 -0
  111. package/dist/test/suites/build/deploy-hook.test.js +169 -0
  112. package/dist/test/suites/build/ensure-target.test.js +14 -2
  113. package/dist/test/suites/build/env-delivery.test.js +19 -10
  114. package/dist/test/suites/build/env-watch.test.js +18 -7
  115. package/dist/test/suites/build/esm-only-dependency.test.js +127 -0
  116. package/dist/test/suites/build/exec-with-limit.test.js +53 -0
  117. package/dist/test/suites/build/finalize-release.test.js +2 -2
  118. package/dist/test/suites/build/get-config.test.js +120 -8
  119. package/dist/test/suites/build/github-utils.test.js +12 -6
  120. package/dist/test/suites/build/license-stamp.test.js +6 -4
  121. package/dist/test/suites/build/manager.test.js +88 -68
  122. package/dist/test/suites/build/manifest-deps.test.js +116 -0
  123. package/dist/test/suites/build/merge-line-files.test.js +27 -4
  124. package/dist/test/suites/build/notarize-artifacts.test.js +135 -0
  125. package/dist/test/suites/build/notarize-tools.test.js +38 -0
  126. package/dist/test/suites/build/notarize.test.js +207 -0
  127. package/dist/test/suites/build/release-pipeline.test.js +38 -60
  128. package/dist/test/suites/build/release-skipped-upload.test.js +107 -0
  129. package/dist/test/suites/build/resolve-icons.test.js +35 -35
  130. package/dist/test/suites/build/runner-job-guard.test.js +182 -0
  131. package/dist/test/suites/build/runner.test.js +635 -5
  132. package/dist/test/suites/build/sentry.test.js +8 -3
  133. package/dist/test/suites/build/setup-scripts.test.js +3 -0
  134. package/dist/test/suites/build/sign-windows.test.js +92 -8
  135. package/dist/test/suites/build/test-stealth.test.js +17 -11
  136. package/dist/test/suites/build/url-helpers.test.js +37 -17
  137. package/dist/test/suites/build/validate-certs.test.js +428 -55
  138. package/dist/test/suites/build/validate-config.test.js +3 -3
  139. package/dist/test/suites/main/auth-flow.test.js +12 -0
  140. package/dist/test/suites/main/auth-persistence.test.js +14 -17
  141. package/dist/test/suites/main/auto-updater.test.js +2 -2
  142. package/dist/test/suites/main/boot-sequence.test.js +1 -1
  143. package/dist/test/suites/main/client-bridge.integration.test.js +5 -82
  144. package/dist/test/suites/main/client-bridge.test.js +19 -6
  145. package/dist/test/suites/main/deep-link.test.js +54 -0
  146. package/dist/test/suites/main/startup-paths-and-ua.test.js +1 -1
  147. package/dist/test/suites/main/url-helpers.test.js +81 -72
  148. package/dist/test/suites/renderer/cross-context-helpers.test.js +19 -16
  149. package/dist/utils/build-pipeline.js +10 -10
  150. package/dist/utils/github.js +12 -51
  151. package/dist/utils/load-env.js +66 -0
  152. package/dist/utils/mode-helpers.js +43 -111
  153. package/dist/utils/platform.js +37 -0
  154. package/dist/utils/runner-env.js +2 -1
  155. package/dist/utils/runner-job-guard.js +149 -0
  156. package/dist/utils/ship-keys.js +52 -0
  157. package/dist/utils/test-stealth.js +5 -3
  158. package/dist/utils/url-helpers.js +33 -17
  159. package/dist/vendor/config/bundle-id.js +53 -0
  160. package/dist/vendor/config/client-config.js +141 -0
  161. package/dist/vendor/config/company.js +334 -15
  162. package/dist/vendor/config/dev-facts.js +48 -0
  163. package/dist/vendor/config/env-delivery.js +231 -9
  164. package/dist/vendor/config/env-retired.js +137 -0
  165. package/dist/vendor/config/env-rules.js +22 -3
  166. package/dist/vendor/config/env-schema.js +234 -117
  167. package/dist/vendor/config/env.js +55 -26
  168. package/dist/vendor/config/environment.js +189 -0
  169. package/dist/vendor/config/hooks.js +13 -11
  170. package/dist/vendor/config/index.js +125 -44
  171. package/dist/vendor/config/load.js +366 -115
  172. package/dist/vendor/config/merge.js +2 -2
  173. package/dist/vendor/config/order.js +3 -3
  174. package/dist/vendor/config/platforms.js +276 -0
  175. package/dist/vendor/config/repo.js +249 -103
  176. package/dist/vendor/config/retired-keys.js +232 -27
  177. package/dist/vendor/config/schema.js +525 -99
  178. package/dist/vendor/config/site-global.js +63 -53
  179. package/dist/vendor/config/targets.js +187 -0
  180. package/dist/vendor/config/validate.js +119 -59
  181. package/dist/vendor/devkit/argv.js +118 -0
  182. package/dist/vendor/devkit/attach-log-file.js +21 -13
  183. package/dist/vendor/devkit/brand-tokens.js +278 -0
  184. package/dist/vendor/devkit/brand-version.js +425 -0
  185. package/dist/vendor/devkit/build-json.js +91 -0
  186. package/dist/vendor/devkit/bundle.js +48 -0
  187. package/dist/vendor/devkit/certificate-expiry.js +108 -0
  188. package/dist/vendor/devkit/certs.js +16 -194
  189. package/dist/vendor/devkit/ci-workflows.js +124 -8
  190. package/dist/vendor/devkit/cli-router.js +3 -3
  191. package/dist/vendor/devkit/defaults-engine.js +69 -7
  192. package/dist/vendor/devkit/deploy-follow.js +297 -0
  193. package/dist/vendor/devkit/deploy-precheck.js +23 -8
  194. package/dist/vendor/devkit/deploy-record.js +11 -27
  195. package/dist/vendor/devkit/deploy-snapshot.js +661 -0
  196. package/dist/vendor/devkit/deploy.js +505 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +172 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +98 -3
  201. package/dist/vendor/devkit/lockfile.js +127 -0
  202. package/dist/vendor/devkit/merge-line-files.js +23 -16
  203. package/dist/vendor/devkit/omega-bin.js +18 -3
  204. package/dist/vendor/devkit/pack-local.js +388 -0
  205. package/dist/vendor/devkit/preludes/index.js +120 -0
  206. package/dist/vendor/devkit/preludes/origin-heal.js +142 -0
  207. package/dist/vendor/devkit/service-account.js +43 -0
  208. package/dist/vendor/devkit/ship-plan.js +112 -0
  209. package/dist/vendor/devkit/signing-env.js +180 -0
  210. package/dist/vendor/devkit/signing-tree.js +92 -0
  211. package/dist/vendor/devkit/target-seams.js +142 -0
  212. package/dist/vendor/devkit/target-secrets.js +231 -57
  213. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  216. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  217. package/dist/vendor/monitoring/env.js +20 -10
  218. package/dist/vendor/monitoring/main.js +1 -1
  219. package/dist/vendor/monitoring/preload.js +1 -1
  220. package/dist/vendor/monitoring/renderer.js +1 -1
  221. package/docs/analytics.md +1 -1
  222. package/docs/auto-updater.md +5 -5
  223. package/docs/boot-sequence.md +1 -1
  224. package/docs/build-system.md +15 -7
  225. package/docs/client-bridge.md +9 -7
  226. package/docs/config-schema.md +4 -4
  227. package/docs/css.md +8 -2
  228. package/docs/deep-link.md +12 -4
  229. package/docs/environment-detection.md +32 -24
  230. package/docs/hooks.md +3 -1
  231. package/docs/icons.md +7 -7
  232. package/docs/index.md +61 -23
  233. package/docs/installer-options.md +24 -21
  234. package/docs/logging.md +5 -5
  235. package/docs/releasing.md +29 -19
  236. package/docs/runner.md +51 -14
  237. package/docs/shared/brands.md +12 -6
  238. package/docs/shared/breaking-changes.md +375 -21
  239. package/docs/shared/config.md +764 -199
  240. package/docs/shared/deploys.md +217 -91
  241. package/docs/shared/icons.md +18 -0
  242. package/docs/shared/local-dev.md +28 -8
  243. package/docs/shared/logging.md +9 -6
  244. package/docs/shared/monitoring.md +27 -13
  245. package/docs/shared/publishing.md +3 -3
  246. package/docs/shared/rulings.md +2 -2
  247. package/docs/shared/testing.md +1 -1
  248. package/docs/shared/theming.md +26 -1
  249. package/docs/shared/translation.md +49 -7
  250. package/docs/shared/updates.md +1 -1
  251. package/docs/signing.md +60 -34
  252. package/docs/test-framework.md +10 -5
  253. package/docs/themes.md +15 -1
  254. package/package.json +18 -12
  255. package/bin/omega-desktop +0 -2
  256. package/dist/commands/push-secrets.js +0 -141
  257. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  258. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  259. package/dist/test/suites/build/push-secrets.test.js +0 -226
  260. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  261. package/dist/utils/deliver-certs.js +0 -69
  262. package/dist/utils/derive-signing-env.js +0 -56
  263. package/dist/utils/resolve-signing-cert.js +0 -175
  264. package/dist/vendor/config/desktop-artifacts.js +0 -110
  265. package/dist/vendor/config/instances.js +0 -208
  266. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  268. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -78,8 +78,8 @@ module.exports = defineCases({
78
78
  jetpack.remove(realish); // a RED run of this case is what would create it
79
79
  try {
80
80
  // Every verb that CHANGES the machine — stop kills real listeners under
81
- // the home, self-update runs a global npm install.
82
- for (const sub of ['install', 'config', 'register-org', 'start', 'restart', 'stop', 'uninstall', 'self-update']) {
81
+ // the home.
82
+ for (const sub of ['install', 'config', 'register-org', 'start', 'restart', 'stop', 'uninstall']) {
83
83
  let threw;
84
84
  try {
85
85
  // `_env: {}` is a fixture for the config walk. It is NOT an answer
@@ -1155,11 +1155,23 @@ module.exports = defineCases({
1155
1155
  jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
1156
1156
  jetpack.write(path.join(home, '_template', 'config.cmd'), 'rem template');
1157
1157
 
1158
+ // The job guard's allow lists are configuration too
1159
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a list
1160
+ // narrowed by hand outlives an uninstall exactly as the `.env` does.
1161
+ // The hook SCRIPT is install state, and the next install writes it back.
1162
+ jetpack.write(path.join(home, 'allowed-repos.txt'), 'Omega-JS-Stack/omega-omega\n');
1163
+ jetpack.write(path.join(home, 'allowed-actors.txt'), 'ianwieds\n');
1164
+ jetpack.write(path.join(home, 'job-started.js'), '// stale copy');
1165
+
1158
1166
  await runner.removeRunnerHomeWithRetry(new Set(), home);
1159
1167
 
1160
- ctx.expect(jetpack.list(home).sort()).toEqual(['.env', 'logs']);
1168
+ ctx.expect(jetpack.list(home).sort()).toEqual(['.env', 'allowed-actors.txt', 'allowed-repos.txt', 'logs']);
1161
1169
  ctx.expect(jetpack.read(path.join(home, '.env'))).toBe('WIN_CSC_KEY_PASSWORD=keep-me');
1162
1170
  ctx.expect(jetpack.read(runnerEnv.runnerLogFile(home))).toBe('# omega log\n');
1171
+ ctx.expect(jetpack.read(path.join(home, 'allowed-repos.txt'))).toBe('Omega-JS-Stack/omega-omega\n');
1172
+
1173
+ jetpack.remove(path.join(home, 'allowed-repos.txt'));
1174
+ jetpack.remove(path.join(home, 'allowed-actors.txt'));
1163
1175
 
1164
1176
  // A kept org dir (its deregistration failed) survives beside them.
1165
1177
  jetpack.write(path.join(home, 'actions-runner-bar', 'config.cmd'), 'rem bar');
@@ -1708,6 +1720,9 @@ module.exports = defineCases({
1708
1720
  const logged = [];
1709
1721
  const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
1710
1722
  try {
1723
+ // Installed and current, so `start` goes straight to its spawns (#937).
1724
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
1725
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
1711
1726
  let threw;
1712
1727
  try {
1713
1728
  await runner({ _: ['runner', 'start'], _home: home, _env: {}, _logger: logger, _prompt: {}, _interactive: false });
@@ -1802,6 +1817,9 @@ module.exports = defineCases({
1802
1817
  const spawned = [];
1803
1818
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
1804
1819
  try {
1820
+ // Installed and current, so `start` goes straight to its spawns (#937).
1821
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
1822
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
1805
1823
  for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
1806
1824
  for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
1807
1825
 
@@ -1851,6 +1869,9 @@ module.exports = defineCases({
1851
1869
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
1852
1870
  const aliveDir = path.join(home, 'actions-runner-acme');
1853
1871
  try {
1872
+ // Installed and current, so `start` goes straight to its spawns (#937).
1873
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
1874
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
1854
1875
  for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
1855
1876
  for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
1856
1877
 
@@ -1895,6 +1916,9 @@ module.exports = defineCases({
1895
1916
  const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
1896
1917
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
1897
1918
  try {
1919
+ // Installed and current, so `start` goes straight to its spawns (#937).
1920
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
1921
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
1898
1922
  for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
1899
1923
  for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
1900
1924
 
@@ -1937,6 +1961,9 @@ module.exports = defineCases({
1937
1961
  const spawned = [];
1938
1962
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
1939
1963
  try {
1964
+ // Installed and current, so `start` goes straight to its spawns (#937).
1965
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
1966
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
1940
1967
  jetpack.dir(path.join(home, 'actions-runner-acme'));
1941
1968
  jetpack.write(shortcut, '@echo off\r\n');
1942
1969
 
@@ -2031,6 +2058,9 @@ module.exports = defineCases({
2031
2058
  const waits = [];
2032
2059
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2033
2060
  try {
2061
+ // Installed and current, so `start` goes straight to its spawns (#937).
2062
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
2063
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
2034
2064
  jetpack.dir(path.join(home, 'actions-runner-acme'));
2035
2065
  jetpack.write(shortcut, '@echo off\r\n');
2036
2066
 
@@ -2080,6 +2110,9 @@ module.exports = defineCases({
2080
2110
  const waits = [];
2081
2111
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2082
2112
  try {
2113
+ // Installed and current, so `start` goes straight to its spawns (#937).
2114
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
2115
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
2083
2116
  jetpack.dir(path.join(home, 'actions-runner-acme'));
2084
2117
  jetpack.write(shortcut, '@echo off\r\n');
2085
2118
 
@@ -2118,7 +2151,7 @@ module.exports = defineCases({
2118
2151
  ctx.expect(startSrc.includes("stdio: 'inherit'")).toBe(false);
2119
2152
  ctx.expect(startSrc.includes('spawnSync')).toBe(false);
2120
2153
  // ...and install hands off to it rather than foregrounding one org.
2121
- ctx.expect(src).toContain('await startServices({ ...options, _home: home })');
2154
+ ctx.expect(src).toContain('await startServices({ ...options, _home: home, _reconcileDone: true })');
2122
2155
 
2123
2156
  // The spawn's own options, pinned the same way: the listener is handed
2124
2157
  // the runner's private HOME ([#807](https://github.com/Omega-JS-Stack/omega/issues/807))
@@ -2154,6 +2187,9 @@ module.exports = defineCases({
2154
2187
  const spawned = [];
2155
2188
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2156
2189
  try {
2190
+ // Installed and current, so `start` goes straight to its spawns (#937).
2191
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
2192
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
2157
2193
  for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
2158
2194
  for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
2159
2195
 
@@ -2206,6 +2242,9 @@ module.exports = defineCases({
2206
2242
  const spawned = [];
2207
2243
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2208
2244
  try {
2245
+ // Installed and current, so `start` goes straight to its spawns (#937).
2246
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
2247
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
2209
2248
  jetpack.dir(path.join(home, 'actions-runner-acme'));
2210
2249
  jetpack.write(shortcut, '@echo off\r\n');
2211
2250
 
@@ -2573,6 +2612,9 @@ module.exports = defineCases({
2573
2612
  const logger = { log: (m) => logged.push(String(m)), warn: (m) => logged.push(String(m)), error: (m) => logged.push(String(m)) };
2574
2613
  const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2575
2614
  try {
2615
+ // Installed and current, so `start` goes straight to its spawns (#937).
2616
+ jetpack.write(path.join(home, '_template', 'config.cmd'), '@echo off\r\n');
2617
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION });
2576
2618
  for (const org of orgs) jetpack.write(path.join(home, `actions-runner-${org}`, 'config.cmd'), '@echo off\r\n');
2577
2619
  for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
2578
2620
  ctx.expect(jetpack.exists(path.join(home, 'home'))).toBe(false);
@@ -2584,10 +2626,21 @@ module.exports = defineCases({
2584
2626
  });
2585
2627
 
2586
2628
  ctx.expect(fs.readFileSync(path.join(home, 'home', '.gitconfig'), 'utf8')).toBe('[core]\n\tautocrlf = false\n');
2629
+ // The job guard is healed on the same pass
2630
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a box
2631
+ // that installed before it would otherwise come online signing for
2632
+ // anything. Both lines, in one `<runner dir>\.env`.
2633
+ const hook = path.join(home, 'job-started.js');
2634
+ ctx.expect(jetpack.exists(hook)).toBe('file');
2587
2635
  for (const org of orgs) {
2588
- ctx.expect(fs.readFileSync(path.join(home, `actions-runner-${org}`, '.env'), 'utf8')).toBe(`HOME=${path.join(home, 'home')}\n`);
2636
+ ctx.expect(fs.readFileSync(path.join(home, `actions-runner-${org}`, '.env'), 'utf8')).toBe(`HOME=${path.join(home, 'home')}\nACTIONS_RUNNER_HOOK_JOB_STARTED=${hook}\n`);
2589
2637
  ctx.expect(logged.join('\n')).toContain(`HOME for ${org}: ${path.join(home, 'home')}`);
2638
+ ctx.expect(logged.join('\n')).toContain(`Job guard for ${org}: ${hook}`);
2590
2639
  }
2640
+ // `start` asks GitHub nothing, so the actor list is the operator's to
2641
+ // fill: the file is there with its header, and an empty list refuses.
2642
+ ctx.expect(jetpack.exists(path.join(home, 'allowed-repos.txt'))).toBe('file');
2643
+ ctx.expect(jetpack.exists(path.join(home, 'allowed-actors.txt'))).toBe('file');
2591
2644
  } finally {
2592
2645
  if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2593
2646
  for (const file of shortcuts) jetpack.remove(file);
@@ -2595,5 +2648,582 @@ module.exports = defineCases({
2595
2648
  }
2596
2649
  },
2597
2650
  },
2651
+ {
2652
+ name: '`start` on a bare box installs: a home with no `_template/config.cmd` hands the whole run to install, once',
2653
+ run: async (ctx) => {
2654
+ // `start` is the one command (#937). A box with nothing installed gets
2655
+ // the install, which itself ends by bringing every org online, so
2656
+ // nothing else of `start` runs: no config walk of its own, no refresh,
2657
+ // no org check, no spawn.
2658
+ const fs = require('fs');
2659
+ const os = require('os');
2660
+ const jetpack = require('fs-jetpack');
2661
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2662
+
2663
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2664
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2665
+ process.env.OMEGA_RUNNER_FORCE = '1';
2666
+ const logged = [];
2667
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2668
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2669
+ const installs = [];
2670
+ const spawned = [];
2671
+ let downloads = 0;
2672
+ let discovers = 0;
2673
+ let registers = 0;
2674
+ try {
2675
+ const options = {
2676
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
2677
+ _listeners: () => [],
2678
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 1 }; },
2679
+ _install: async (o) => { installs.push(o); },
2680
+ _download: async () => { downloads++; },
2681
+ _discoverOrgs: async () => { discovers++; return []; },
2682
+ _registerOrg: async () => { registers++; },
2683
+ };
2684
+ await runner(options);
2685
+
2686
+ ctx.expect(installs.length).toBe(1);
2687
+ ctx.expect(installs[0]).toBe(options); // the same options, untouched
2688
+ ctx.expect(spawned).toEqual([]);
2689
+ ctx.expect(downloads).toBe(0);
2690
+ ctx.expect(discovers).toBe(0);
2691
+ ctx.expect(registers).toBe(0);
2692
+ ctx.expect(jetpack.exists(path.join(home, '.env'))).toBe(false); // start's own config walk never ran
2693
+ } finally {
2694
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2695
+ jetpack.remove(home);
2696
+ }
2697
+ },
2698
+ },
2699
+ {
2700
+ name: '`start` refreshes a stale actions/runner in place: the template is re-downloaded and copied over each stopped org, its registration kept',
2701
+ run: async (ctx) => {
2702
+ // A bumped ACTIONS_RUNNER_VERSION used to mean `install`, which tears
2703
+ // the box down. The template is re-downloaded and laid over each org dir
2704
+ // instead; `.runner`, `.credentials` and `.env` are the registration and
2705
+ // the template never carries them, so they come through byte-identical.
2706
+ const fs = require('fs');
2707
+ const os = require('os');
2708
+ const jetpack = require('fs-jetpack');
2709
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2710
+
2711
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2712
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2713
+ const origExit = process.exitCode;
2714
+ process.env.OMEGA_RUNNER_FORCE = '1';
2715
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
2716
+ const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
2717
+ const logged = [];
2718
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2719
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2720
+ const templateDir = path.join(home, '_template');
2721
+ const orgDir = path.join(home, 'actions-runner-acme');
2722
+ const downloads = [];
2723
+ const spawned = [];
2724
+ // The registration files, exactly as the preflight leaves `.env`, so a
2725
+ // byte change can only have come from the copy.
2726
+ const kept = {
2727
+ '.runner': '{"agentName":"omega-runner-box-acme"}',
2728
+ '.credentials': '{"scheme":"OAuth"}',
2729
+ '.env': `https_proxy=http://proxy\nHOME=${path.join(home, 'home')}\nACTIONS_RUNNER_HOOK_JOB_STARTED=${path.join(home, 'job-started.js')}\n`,
2730
+ };
2731
+ try {
2732
+ jetpack.write(path.join(templateDir, 'config.cmd'), 'old config');
2733
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: '2.300.0', registeredOrgs: ['acme'] });
2734
+ jetpack.write(path.join(orgDir, 'config.cmd'), 'old config');
2735
+ jetpack.write(path.join(orgDir, 'bin', 'Runner.Listener.exe'), 'old listener');
2736
+ for (const [name, content] of Object.entries(kept)) jetpack.write(path.join(orgDir, name), content);
2737
+ jetpack.write(shortcut, '@echo off\r\n');
2738
+
2739
+ await runner({
2740
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
2741
+ _listeners: () => [],
2742
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 5150 }; },
2743
+ _download: async (dir, h) => {
2744
+ downloads.push([dir, h]);
2745
+ jetpack.write(path.join(dir, 'config.cmd'), 'new config');
2746
+ jetpack.write(path.join(dir, 'bin', 'Runner.Listener.exe'), 'new listener');
2747
+ },
2748
+ _discoverOrgs: async () => ['acme'],
2749
+ _registerOrg: async () => { throw new Error('nothing to register'); },
2750
+ });
2751
+
2752
+ ctx.expect(downloads).toEqual([[templateDir, home]]);
2753
+ ctx.expect(fs.readFileSync(path.join(orgDir, 'config.cmd'), 'utf8')).toBe('new config');
2754
+ ctx.expect(fs.readFileSync(path.join(orgDir, 'bin', 'Runner.Listener.exe'), 'utf8')).toBe('new listener');
2755
+ for (const [name, content] of Object.entries(kept)) {
2756
+ ctx.expect(fs.readFileSync(path.join(orgDir, name), 'utf8')).toBe(content);
2757
+ }
2758
+ ctx.expect(jetpack.read(path.join(home, 'config.json'), 'json').actionsRunnerVersion).toBe(runner.ACTIONS_RUNNER_VERSION);
2759
+ ctx.expect(spawned).toEqual([orgDir]); // and the org still comes online
2760
+ ctx.expect(process.exitCode).toBe(origExit);
2761
+ } finally {
2762
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2763
+ process.exitCode = origExit;
2764
+ jetpack.remove(shortcut);
2765
+ jetpack.remove(home);
2766
+ }
2767
+ },
2768
+ },
2769
+ {
2770
+ name: '`start` never refreshes an org whose listener is alive: its binaries are locked, so it names `restart` and the version stays unrecorded',
2771
+ run: async (ctx) => {
2772
+ // A live listener holds its dir's binaries open. Copying over them is a
2773
+ // half-updated runner, so that org is skipped with the command that
2774
+ // stops it first, and the box is not called refreshed until every org
2775
+ // dir took the new version.
2776
+ const fs = require('fs');
2777
+ const os = require('os');
2778
+ const jetpack = require('fs-jetpack');
2779
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2780
+
2781
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2782
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2783
+ const origExit = process.exitCode;
2784
+ process.env.OMEGA_RUNNER_FORCE = '1';
2785
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
2786
+ const orgs = ['acme', 'zeta'];
2787
+ const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
2788
+ const logged = [];
2789
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2790
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2791
+ const templateDir = path.join(home, '_template');
2792
+ const aliveDir = path.join(home, 'actions-runner-acme');
2793
+ const stoppedDir = path.join(home, 'actions-runner-zeta');
2794
+ try {
2795
+ jetpack.write(path.join(templateDir, 'config.cmd'), 'old config');
2796
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: '2.300.0', registeredOrgs: orgs });
2797
+ for (const dir of [aliveDir, stoppedDir]) jetpack.write(path.join(dir, 'config.cmd'), 'old config');
2798
+ for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
2799
+
2800
+ await runner({
2801
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
2802
+ _listeners: (dir) => (dir === aliveDir ? [{ pid: 4242, sessionId: 1, execPath: path.join(aliveDir, 'bin', 'Runner.Listener.exe') }] : []),
2803
+ _spawn: () => ({ ok: true, pid: 6000 }),
2804
+ _download: async (dir) => { jetpack.write(path.join(dir, 'config.cmd'), 'new config'); },
2805
+ _discoverOrgs: async () => orgs,
2806
+ _registerOrg: async () => { throw new Error('nothing to register'); },
2807
+ });
2808
+
2809
+ ctx.expect(fs.readFileSync(path.join(aliveDir, 'config.cmd'), 'utf8')).toBe('old config'); // untouched
2810
+ ctx.expect(fs.readFileSync(path.join(stoppedDir, 'config.cmd'), 'utf8')).toBe('new config'); // the stopped one took it
2811
+ const skip = logged.map((l) => l[1]).filter((l) => l.includes('npx omega runner restart'));
2812
+ ctx.expect(skip.length).toBe(1);
2813
+ ctx.expect(skip[0]).toContain('acme');
2814
+ ctx.expect(jetpack.read(path.join(home, 'config.json'), 'json').actionsRunnerVersion).toBe('2.300.0');
2815
+ } finally {
2816
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2817
+ process.exitCode = origExit;
2818
+ for (const file of shortcuts) jetpack.remove(file);
2819
+ jetpack.remove(home);
2820
+ }
2821
+ },
2822
+ },
2823
+ {
2824
+ name: '`start` downloads nothing when the recorded actions/runner version is the pinned one',
2825
+ run: async (ctx) => {
2826
+ // The refresh is for a stale box only: a current one never pays for a
2827
+ // 150 MB download on every start.
2828
+ const fs = require('fs');
2829
+ const os = require('os');
2830
+ const jetpack = require('fs-jetpack');
2831
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2832
+
2833
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2834
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2835
+ const origExit = process.exitCode;
2836
+ process.env.OMEGA_RUNNER_FORCE = '1';
2837
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
2838
+ const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
2839
+ const logged = [];
2840
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2841
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2842
+ const orgDir = path.join(home, 'actions-runner-acme');
2843
+ const spawned = [];
2844
+ let downloads = 0;
2845
+ try {
2846
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'config');
2847
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION, registeredOrgs: ['acme'] });
2848
+ jetpack.write(path.join(orgDir, 'config.cmd'), 'config');
2849
+ jetpack.write(shortcut, '@echo off\r\n');
2850
+
2851
+ await runner({
2852
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
2853
+ _listeners: () => [],
2854
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 7 }; },
2855
+ _download: async () => { downloads++; },
2856
+ _discoverOrgs: async () => ['acme'],
2857
+ _registerOrg: async () => { throw new Error('nothing to register'); },
2858
+ });
2859
+
2860
+ ctx.expect(downloads).toBe(0);
2861
+ ctx.expect(spawned).toEqual([orgDir]);
2862
+ } finally {
2863
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2864
+ process.exitCode = origExit;
2865
+ jetpack.remove(shortcut);
2866
+ jetpack.remove(home);
2867
+ }
2868
+ },
2869
+ },
2870
+ {
2871
+ name: '`start` registers an admin org the box does not serve yet, honouring OMEGA_RUNNER_ORGS, and nothing already registered',
2872
+ run: async (ctx) => {
2873
+ // A new admin org used to need `install` again. `start` registers the
2874
+ // missing one itself: named by the filter, not yet in registeredOrgs.
2875
+ const fs = require('fs');
2876
+ const os = require('os');
2877
+ const jetpack = require('fs-jetpack');
2878
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2879
+
2880
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2881
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2882
+ const origExit = process.exitCode;
2883
+ process.env.OMEGA_RUNNER_FORCE = '1';
2884
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
2885
+ const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
2886
+ const logged = [];
2887
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2888
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2889
+ const registered = [];
2890
+ try {
2891
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'config');
2892
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION, registeredOrgs: ['acme'] });
2893
+ jetpack.write(path.join(home, 'actions-runner-acme', 'config.cmd'), 'config');
2894
+ jetpack.write(shortcut, '@echo off\r\n');
2895
+
2896
+ await runner({
2897
+ _: ['runner', 'start'], _home: home, _env: { ...filled, OMEGA_RUNNER_ORGS: 'acme, Beta' }, _logger: logger, _interactive: false, _prompt: {},
2898
+ _listeners: () => [],
2899
+ _spawn: () => ({ ok: true, pid: 8 }),
2900
+ _download: async () => { throw new Error('the version matches: no download'); },
2901
+ _discoverOrgs: async () => ['acme', 'beta', 'gamma'],
2902
+ _registerOrg: async (o) => { registered.push({ org: o._[2], home: o._home }); },
2903
+ });
2904
+
2905
+ // beta is named and new; acme is already served; gamma is not named.
2906
+ ctx.expect(registered).toEqual([{ org: 'beta', home }]);
2907
+ ctx.expect(process.exitCode).toBe(origExit);
2908
+ } finally {
2909
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2910
+ process.exitCode = origExit;
2911
+ jetpack.remove(shortcut);
2912
+ jetpack.remove(home);
2913
+ }
2914
+ },
2915
+ },
2916
+ {
2917
+ name: '`start` works offline: an org discovery that throws is ONE skip line, and every registered org still comes up',
2918
+ run: async (ctx) => {
2919
+ // The org check is the only part of `start` that needs GitHub. A box
2920
+ // with no network still gets its listeners.
2921
+ const fs = require('fs');
2922
+ const os = require('os');
2923
+ const jetpack = require('fs-jetpack');
2924
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2925
+
2926
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2927
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2928
+ const origExit = process.exitCode;
2929
+ process.env.OMEGA_RUNNER_FORCE = '1';
2930
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
2931
+ const orgs = ['acme', 'zeta'];
2932
+ const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
2933
+ const logged = [];
2934
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2935
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2936
+ const spawned = [];
2937
+ let registers = 0;
2938
+ try {
2939
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'config');
2940
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION, registeredOrgs: orgs });
2941
+ for (const org of orgs) jetpack.write(path.join(home, `actions-runner-${org}`, 'config.cmd'), 'config');
2942
+ for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
2943
+
2944
+ await runner({
2945
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
2946
+ _listeners: () => [],
2947
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 9 }; },
2948
+ _download: async () => { throw new Error('the version matches: no download'); },
2949
+ _discoverOrgs: async () => { throw new Error('getaddrinfo ENOTFOUND api.github.com'); },
2950
+ _registerOrg: async () => { registers++; },
2951
+ });
2952
+
2953
+ const skips = logged.map((l) => l[1]).filter((l) => /org check skipped/i.test(l));
2954
+ ctx.expect(skips.length).toBe(1);
2955
+ ctx.expect(skips[0]).toContain('ENOTFOUND');
2956
+ ctx.expect(registers).toBe(0);
2957
+ ctx.expect(spawned).toEqual(orgs.map((org) => path.join(home, `actions-runner-${org}`)));
2958
+ ctx.expect(process.exitCode).toBe(origExit);
2959
+ } finally {
2960
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
2961
+ process.exitCode = origExit;
2962
+ for (const file of shortcuts) jetpack.remove(file);
2963
+ jetpack.remove(home);
2964
+ }
2965
+ },
2966
+ },
2967
+ {
2968
+ name: '`start` in a test run never asks GitHub unless both GitHub seams are injected',
2969
+ run: async (ctx) => {
2970
+ // Registration is not home-scoped: a real `registerOrg` with a real
2971
+ // token registers the org on GitHub whatever scratch home the case
2972
+ // made. A test run without `_discoverOrgs` AND `_registerOrg` skips
2973
+ // the org check on one line, the way `uninstall` skips the surfaces no
2974
+ // path redirects.
2975
+ const fs = require('fs');
2976
+ const os = require('os');
2977
+ const jetpack = require('fs-jetpack');
2978
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
2979
+
2980
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
2981
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
2982
+ const origExit = process.exitCode;
2983
+ process.env.OMEGA_RUNNER_FORCE = '1';
2984
+ const logged = [];
2985
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
2986
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
2987
+ let discovers = 0;
2988
+ try {
2989
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'config');
2990
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: runner.ACTIONS_RUNNER_VERSION, registeredOrgs: [] });
2991
+
2992
+ await runner({
2993
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
2994
+ _listeners: () => [],
2995
+ _spawn: () => ({ ok: true, pid: 10 }),
2996
+ _discoverOrgs: async () => { discovers++; return ['acme']; }, // no _registerOrg
2997
+ });
2998
+
2999
+ ctx.expect(discovers).toBe(0);
3000
+ const skips = logged.map((l) => l[1]).filter((l) => /org check skipped/i.test(l));
3001
+ ctx.expect(skips.length).toBe(1);
3002
+ ctx.expect(skips[0]).toContain('_registerOrg');
3003
+ } finally {
3004
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
3005
+ process.exitCode = origExit;
3006
+ jetpack.remove(home);
3007
+ }
3008
+ },
3009
+ },
3010
+ {
3011
+ name: '`self-update` is gone: the dispatcher calls it an unknown subcommand',
3012
+ run: async (ctx) => {
3013
+ // Every runner verb runs as `npx omega runner <verb>` from the box's
3014
+ // clone, so there is no global package for it to update (#937). The
3015
+ // home is one the test guard refuses, so no build of this command,
3016
+ // however old, can run a global npm install from this case.
3017
+ const os = require('os');
3018
+ const jetpack = require('fs-jetpack');
3019
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
3020
+
3021
+ const realish = path.join(os.homedir(), 'omega-runner-NOT-A-SCRATCH');
3022
+ let threw;
3023
+ try {
3024
+ await runner({ _: ['runner', 'self-update'], _home: realish });
3025
+ } catch (e) { threw = e; }
3026
+ ctx.expect(threw).toBeDefined();
3027
+ ctx.expect(threw.message).toBe('Unknown runner subcommand: self-update');
3028
+ ctx.expect(jetpack.exists(realish)).toBe(false);
3029
+ },
3030
+ },
3031
+ {
3032
+ name: '`start` called from install (`_reconcileDone`) skips the reconcile: no download and no org check, even on a stale version with a missing org',
3033
+ run: async (ctx) => {
3034
+ // install has just downloaded the template and registered every org, so
3035
+ // its closing start has nothing to reconcile, and a retry of an org that
3036
+ // failed there would print its error a second time.
3037
+ const fs = require('fs');
3038
+ const os = require('os');
3039
+ const jetpack = require('fs-jetpack');
3040
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
3041
+
3042
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
3043
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
3044
+ const origExit = process.exitCode;
3045
+ process.env.OMEGA_RUNNER_FORCE = '1';
3046
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
3047
+ const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
3048
+ const logged = [];
3049
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
3050
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
3051
+ const orgDir = path.join(home, 'actions-runner-acme');
3052
+ const spawned = [];
3053
+ let downloads = 0;
3054
+ let discovers = 0;
3055
+ let registers = 0;
3056
+ try {
3057
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'config');
3058
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: '2.300.0', registeredOrgs: ['acme'] });
3059
+ jetpack.write(path.join(orgDir, 'config.cmd'), 'config');
3060
+ jetpack.write(shortcut, '@echo off\r\n');
3061
+
3062
+ await runner({
3063
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
3064
+ _reconcileDone: true,
3065
+ _listeners: () => [],
3066
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 11 }; },
3067
+ _download: async () => { downloads++; },
3068
+ _discoverOrgs: async () => { discovers++; return ['acme', 'beta']; },
3069
+ _registerOrg: async () => { registers++; },
3070
+ });
3071
+
3072
+ ctx.expect(downloads).toBe(0);
3073
+ ctx.expect(discovers).toBe(0);
3074
+ ctx.expect(registers).toBe(0);
3075
+ ctx.expect(jetpack.read(path.join(home, 'config.json'), 'json').actionsRunnerVersion).toBe('2.300.0');
3076
+ ctx.expect(spawned).toEqual([orgDir]); // the spawns still run
3077
+ } finally {
3078
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
3079
+ process.exitCode = origExit;
3080
+ jetpack.remove(shortcut);
3081
+ jetpack.remove(home);
3082
+ }
3083
+ },
3084
+ },
3085
+ {
3086
+ name: '`start` survives a refresh download that fails: one warn, no version recorded, every org still comes up',
3087
+ run: async (ctx) => {
3088
+ // A stale box with no network must still come online: the refresh is
3089
+ // skipped until the next start, never the whole run.
3090
+ const fs = require('fs');
3091
+ const os = require('os');
3092
+ const jetpack = require('fs-jetpack');
3093
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
3094
+
3095
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
3096
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
3097
+ const origExit = process.exitCode;
3098
+ process.env.OMEGA_RUNNER_FORCE = '1';
3099
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
3100
+ const orgs = ['acme', 'zeta'];
3101
+ const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
3102
+ const logged = [];
3103
+ const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
3104
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
3105
+ const spawned = [];
3106
+ try {
3107
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'old config');
3108
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: '2.300.0', registeredOrgs: orgs });
3109
+ for (const org of orgs) jetpack.write(path.join(home, `actions-runner-${org}`, 'config.cmd'), 'old config');
3110
+ for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
3111
+
3112
+ await runner({
3113
+ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
3114
+ _listeners: () => [],
3115
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 12 }; },
3116
+ _download: async () => { throw new Error('curl exit 6'); },
3117
+ _discoverOrgs: async () => orgs,
3118
+ _registerOrg: async () => { throw new Error('nothing to register'); },
3119
+ });
3120
+
3121
+ const warns = logged.filter((l) => l[0] === 'warn' && l[1].includes('curl exit 6'));
3122
+ ctx.expect(warns.length).toBe(1);
3123
+ ctx.expect(jetpack.read(path.join(home, 'config.json'), 'json').actionsRunnerVersion).toBe('2.300.0');
3124
+ for (const org of orgs) ctx.expect(fs.readFileSync(path.join(home, `actions-runner-${org}`, 'config.cmd'), 'utf8')).toBe('old config');
3125
+ ctx.expect(spawned).toEqual(orgs.map((org) => path.join(home, `actions-runner-${org}`)));
3126
+ ctx.expect(process.exitCode).toBe(origExit);
3127
+ } finally {
3128
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
3129
+ process.exitCode = origExit;
3130
+ for (const file of shortcuts) jetpack.remove(file);
3131
+ jetpack.remove(home);
3132
+ }
3133
+ },
3134
+ },
3135
+ {
3136
+ name: '`start` on a bare box in a test run refuses without an injected `_install`, and installs nothing',
3137
+ run: async (ctx) => {
3138
+ // The real install downloads, discovers and registers against GitHub.
3139
+ // A test run reaches it only through the seam, the way deregistration
3140
+ // refuses without its own.
3141
+ const fs = require('fs');
3142
+ const os = require('os');
3143
+ const jetpack = require('fs-jetpack');
3144
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
3145
+
3146
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
3147
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
3148
+ process.env.OMEGA_RUNNER_FORCE = '1';
3149
+ const logger = { log: () => {}, warn: () => {}, error: () => {} };
3150
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
3151
+ try {
3152
+ let threw;
3153
+ try {
3154
+ await runner({ _: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {}, _listeners: () => [], _spawn: () => ({ ok: true, pid: 13 }) });
3155
+ } catch (e) { threw = e; }
3156
+ ctx.expect(threw).toBeDefined();
3157
+ ctx.expect(threw.message).toContain('_install');
3158
+ ctx.expect(jetpack.exists(path.join(home, '_template'))).toBe(false);
3159
+ ctx.expect(jetpack.exists(path.join(home, '.env'))).toBe(false);
3160
+ } finally {
3161
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
3162
+ jetpack.remove(home);
3163
+ }
3164
+ },
3165
+ },
3166
+ {
3167
+ name: '`restart` refreshes a stale actions/runner AFTER its stop, once the listener is gone, and records the version',
3168
+ run: async (ctx) => {
3169
+ // The reconcile is not gated on restart's preflight: after the stop is
3170
+ // exactly when an org's binaries are free to be replaced.
3171
+ const fs = require('fs');
3172
+ const os = require('os');
3173
+ const jetpack = require('fs-jetpack');
3174
+ const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
3175
+
3176
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
3177
+ const origForce = process.env.OMEGA_RUNNER_FORCE;
3178
+ const origExit = process.exitCode;
3179
+ process.env.OMEGA_RUNNER_FORCE = '1';
3180
+ const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
3181
+ const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
3182
+ const order = [];
3183
+ const logger = {
3184
+ log: (m) => { if (String(m).includes('already stopped')) order.push('stop'); },
3185
+ warn: () => {},
3186
+ error: () => {},
3187
+ };
3188
+ const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
3189
+ const orgDir = path.join(home, 'actions-runner-acme');
3190
+ const spawned = [];
3191
+ try {
3192
+ jetpack.write(path.join(home, '_template', 'config.cmd'), 'old config');
3193
+ jetpack.write(path.join(home, 'config.json'), { actionsRunnerVersion: '2.300.0', registeredOrgs: ['acme'] });
3194
+ jetpack.write(path.join(orgDir, 'config.cmd'), 'old config');
3195
+ jetpack.write(shortcut, '@echo off\r\n');
3196
+
3197
+ // Alive for the first two polls, gone after.
3198
+ let looks = 0;
3199
+ await runner({
3200
+ _: ['runner', 'restart'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
3201
+ _delay: async () => {},
3202
+ _listeners: () => {
3203
+ const alive = ++looks <= 2;
3204
+ order.push(alive ? 'alive' : 'gone');
3205
+ return alive ? [{ pid: 909, sessionId: 1, execPath: path.join(orgDir, 'bin', 'Runner.Listener.exe') }] : [];
3206
+ },
3207
+ _spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 14 }; },
3208
+ _download: async (dir) => { order.push('download'); jetpack.write(path.join(dir, 'config.cmd'), 'new config'); },
3209
+ _discoverOrgs: async () => ['acme'],
3210
+ _registerOrg: async () => { throw new Error('nothing to register'); },
3211
+ });
3212
+
3213
+ ctx.expect(order.indexOf('stop')).toBeGreaterThan(-1);
3214
+ ctx.expect(order.indexOf('download')).toBeGreaterThan(order.indexOf('stop'));
3215
+ ctx.expect(order.indexOf('download')).toBeGreaterThan(order.lastIndexOf('alive'));
3216
+ ctx.expect(fs.readFileSync(path.join(orgDir, 'config.cmd'), 'utf8')).toBe('new config');
3217
+ ctx.expect(jetpack.read(path.join(home, 'config.json'), 'json').actionsRunnerVersion).toBe(runner.ACTIONS_RUNNER_VERSION);
3218
+ ctx.expect(spawned).toEqual([orgDir]);
3219
+ ctx.expect(process.exitCode).toBe(origExit);
3220
+ } finally {
3221
+ if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
3222
+ process.exitCode = origExit;
3223
+ jetpack.remove(shortcut);
3224
+ jetpack.remove(home);
3225
+ }
3226
+ },
3227
+ },
2598
3228
  ],
2599
3229
  });