@omega.js/desktop 0.50.0 → 0.51.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 (267) hide show
  1. package/README.md +1 -1
  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 +42 -5
  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 +144 -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 +25 -2
  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 +121 -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 +226 -104
  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 +264 -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 +445 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +95 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +47 -0
  201. package/dist/vendor/devkit/merge-line-files.js +23 -16
  202. package/dist/vendor/devkit/omega-bin.js +18 -3
  203. package/dist/vendor/devkit/pack-local.js +391 -0
  204. package/dist/vendor/devkit/preludes/index.js +120 -0
  205. package/dist/vendor/devkit/preludes/origin-heal.js +156 -0
  206. package/dist/vendor/devkit/service-account.js +43 -0
  207. package/dist/vendor/devkit/ship-plan.js +112 -0
  208. package/dist/vendor/devkit/signing-env.js +180 -0
  209. package/dist/vendor/devkit/signing-tree.js +92 -0
  210. package/dist/vendor/devkit/target-seams.js +142 -0
  211. package/dist/vendor/devkit/target-secrets.js +235 -50
  212. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  213. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  216. package/dist/vendor/monitoring/env.js +20 -10
  217. package/dist/vendor/monitoring/main.js +1 -1
  218. package/dist/vendor/monitoring/preload.js +1 -1
  219. package/dist/vendor/monitoring/renderer.js +1 -1
  220. package/docs/analytics.md +1 -1
  221. package/docs/auto-updater.md +5 -5
  222. package/docs/boot-sequence.md +1 -1
  223. package/docs/build-system.md +15 -7
  224. package/docs/client-bridge.md +9 -7
  225. package/docs/config-schema.md +4 -4
  226. package/docs/css.md +8 -2
  227. package/docs/deep-link.md +12 -4
  228. package/docs/environment-detection.md +32 -24
  229. package/docs/hooks.md +3 -1
  230. package/docs/icons.md +7 -7
  231. package/docs/index.md +61 -23
  232. package/docs/installer-options.md +24 -21
  233. package/docs/logging.md +5 -5
  234. package/docs/releasing.md +25 -17
  235. package/docs/runner.md +40 -5
  236. package/docs/shared/brands.md +12 -6
  237. package/docs/shared/breaking-changes.md +375 -21
  238. package/docs/shared/config.md +757 -199
  239. package/docs/shared/deploys.md +194 -91
  240. package/docs/shared/icons.md +18 -0
  241. package/docs/shared/local-dev.md +24 -6
  242. package/docs/shared/logging.md +9 -6
  243. package/docs/shared/monitoring.md +27 -13
  244. package/docs/shared/publishing.md +3 -3
  245. package/docs/shared/rulings.md +2 -2
  246. package/docs/shared/testing.md +1 -1
  247. package/docs/shared/theming.md +26 -1
  248. package/docs/shared/translation.md +49 -7
  249. package/docs/shared/updates.md +1 -1
  250. package/docs/signing.md +59 -33
  251. package/docs/test-framework.md +10 -5
  252. package/docs/themes.md +15 -1
  253. package/package.json +18 -12
  254. package/bin/omega-desktop +0 -2
  255. package/dist/commands/push-secrets.js +0 -141
  256. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  257. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  258. package/dist/test/suites/build/push-secrets.test.js +0 -226
  259. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  260. package/dist/utils/deliver-certs.js +0 -69
  261. package/dist/utils/derive-signing-env.js +0 -56
  262. package/dist/utils/resolve-signing-cert.js +0 -175
  263. package/dist/vendor/config/desktop-artifacts.js +0 -110
  264. package/dist/vendor/config/instances.js +0 -208
  265. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  266. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -0,0 +1,241 @@
1
+ // Build-layer test for `omega deploy --direct`'s platform guard
2
+ // ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
3
+ //
4
+ // The local lane is now a FLAG, not a detection: a brand tree carrying a
5
+ // `file:` @omega.js spec used to switch itself here, which took the CI lane
6
+ // away from exactly the brands that need it most (a linked brand now packs its
7
+ // frameworks into the snapshot the runner installs).
8
+ //
9
+ // And a local publish can only build what THIS machine can build. Electron
10
+ // cross-building is what the CI matrix exists for, so a `--platforms` naming
11
+ // anything else is refused by name rather than quietly building the host's.
12
+ //
13
+ // The laptop and the runner are ONE pipeline in two environments (#865), so
14
+ // the last case PINS them to each other: the command the workflow's publishing
15
+ // legs run, the script that command expands to, and the call `--direct` makes
16
+ // are read from the files themselves rather than restated here.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const Module = require('module');
21
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
22
+ const attachLogFile = require('../../../vendor/devkit/attach-log-file.js');
23
+ const deployRecord = require('../../../vendor/devkit/deploy-record.js');
24
+
25
+ const COMMANDS = path.join(__dirname, '..', '..', '..', 'commands');
26
+ const DEPLOY = path.join(COMMANDS, 'deploy.js');
27
+ const WORKFLOW = path.join(__dirname, '..', '..', '..', 'defaults', '.github', 'workflows', 'build.yml');
28
+
29
+ // The scripts every target gets from the scaffold, read from the one place that
30
+ // declares them: expanding the workflow's command through THESE is what ties
31
+ // the runner's step to the verb this laptop runs.
32
+ const SCRIPTS = require(path.join(__dirname, '..', '..', '..', '..', 'package.json')).projectScripts;
33
+
34
+ const { directPlatform } = require(DEPLOY);
35
+
36
+ /**
37
+ * The publishing legs of the CI build, read from the workflow the scaffold
38
+ * writes: the mac and linux steps, which are the ones that publish (windows
39
+ * packages unsigned and `finalize-release` closes its release). A step's own
40
+ * command is ONE line; the `run: |` blocks around them are the leg's setup
41
+ * (decoding the signing assets, installing snapcraft), never the publish.
42
+ *
43
+ * @returns {Array<{ os: string, command: string }>} Every publishing leg's command, in file order.
44
+ */
45
+ function ciPublishLegs() {
46
+ const lines = fs.readFileSync(WORKFLOW, 'utf8').split('\n').map((line) => line.trim());
47
+ const legs = [];
48
+ let os = null;
49
+
50
+ for (const line of lines) {
51
+ // A `- ` opens the next step, so the leg an `if:` names ends there.
52
+ if (line.startsWith('- ')) os = null;
53
+
54
+ const gate = /^(?:- )?if: matrix\.os == '(macos-latest|ubuntu-latest)'$/.exec(line);
55
+ if (gate) os = gate[1];
56
+
57
+ const run = /^(?:- )?run: (.+)$/.exec(line);
58
+ if (os && run && run[1] !== '|') legs.push({ os, command: run[1] });
59
+ }
60
+
61
+ return legs;
62
+ }
63
+
64
+ /**
65
+ * Swap ONE module for a recorder and hand back the undo. The precheck is the
66
+ * network boundary (it publishes this target's signing certs as Actions
67
+ * secrets), so proving `--direct` never reaches it means standing exactly
68
+ * there: everything else in the verb is the real code.
69
+ *
70
+ * @param {string} file - The module to replace.
71
+ * @param {object} exports - What it exports for the duration.
72
+ * @returns {Function} The restore.
73
+ */
74
+ function stubModule(file, exports) {
75
+ const resolved = require.resolve(file);
76
+ const previous = require.cache[resolved];
77
+ const stub = new Module(resolved);
78
+
79
+ stub.filename = resolved;
80
+ stub.loaded = true;
81
+ stub.exports = exports;
82
+ require.cache[resolved] = stub;
83
+
84
+ return () => {
85
+ if (previous) require.cache[resolved] = previous;
86
+ else delete require.cache[resolved];
87
+ };
88
+ }
89
+
90
+ module.exports = defineCases({
91
+ type: 'suite',
92
+ layer: 'build',
93
+ description: 'deploy --direct: the host platform, and only the host platform',
94
+ tests: [
95
+ {
96
+ name: 'no --platforms builds the host, in the workflow input vocabulary',
97
+ run: (ctx) => {
98
+ ctx.expect(directPlatform(undefined, 'darwin')).toBe('mac');
99
+ ctx.expect(directPlatform(undefined, 'win32')).toBe('windows');
100
+ ctx.expect(directPlatform(undefined, 'linux')).toBe('linux');
101
+ },
102
+ },
103
+ {
104
+ name: 'the host platform in OMEGA\'s own vocabulary is accepted, in any case (#867)',
105
+ run: (ctx) => {
106
+ ctx.expect(directPlatform('mac', 'darwin')).toBe('mac');
107
+ ctx.expect(directPlatform('MAC', 'darwin')).toBe('mac');
108
+ ctx.expect(directPlatform('windows', 'win32')).toBe('windows');
109
+ ctx.expect(directPlatform('WINDOWS', 'win32')).toBe('windows');
110
+ ctx.expect(directPlatform('linux', 'linux')).toBe('linux');
111
+ },
112
+ },
113
+ {
114
+ name: 'a host NAME from another vocabulary is refused, naming the one OMEGA speaks (#867)',
115
+ run: (ctx) => {
116
+ // The flag speaks mac | windows | linux and nothing else: the node and
117
+ // runner spellings of the SAME host are foreign words here, and a
118
+ // refusal names the host in the vocabulary the flag does accept.
119
+ ctx.expect(() => directPlatform('darwin', 'darwin')).toThrow(/can only build mac: darwin/);
120
+ ctx.expect(() => directPlatform('win32', 'win32')).toThrow(/can only build windows: win32/);
121
+ ctx.expect(() => directPlatform('ubuntu', 'linux')).toThrow(/can only build linux: ubuntu/);
122
+ },
123
+ },
124
+ {
125
+ name: 'a platform this machine cannot build is REFUSED, never silently swapped for the host',
126
+ run: (ctx) => {
127
+ ctx.expect(() => directPlatform('windows', 'darwin')).toThrow(/CI-only/);
128
+ ctx.expect(() => directPlatform('mac,linux', 'darwin')).toThrow(/linux/);
129
+ // 'all' names three platforms, and a laptop is one of them
130
+ ctx.expect(() => directPlatform('all', 'darwin')).toThrow(/CI-only/);
131
+ },
132
+ },
133
+ {
134
+ name: 'an unknown host refuses too, pointing at the CI lane',
135
+ run: (ctx) => {
136
+ ctx.expect(() => directPlatform(undefined, 'aix')).toThrow(/Dispatch the CI build/);
137
+ },
138
+ },
139
+ {
140
+ name: '--direct never runs the NETWORK precheck: a local deploy publishes no secrets (#872)',
141
+ run: async (ctx) => {
142
+ const ran = [];
143
+ const restores = [
144
+ stubModule(path.join(COMMANDS, 'lib', 'ensure-target.js'), { ensureTarget: async () => ran.push('ensure-target') }),
145
+ stubModule(path.join(COMMANDS, 'lib', 'deploy-precheck.js'), { deployPrecheck: async () => ran.push('precheck') }),
146
+ ];
147
+ const resolved = require.resolve(DEPLOY);
148
+ const cached = require.cache[resolved];
149
+ delete require.cache[resolved];
150
+
151
+ try {
152
+ const deploy = require(resolved);
153
+
154
+ // A platform no host can build: the direct lane refuses it, which is
155
+ // the proof the run REACHED that lane. What must not have happened on
156
+ // the way is the precheck, which publishes the signing certs to the
157
+ // repo's Actions secrets and needs a `gh` session to do it.
158
+ await ctx.expect(() => deploy({ direct: true, platforms: 'atari' })).toThrow(/CI-only/);
159
+ ctx.expect(ran).toEqual(['ensure-target']);
160
+ } finally {
161
+ // The verb tees this process' writers now (#873): hand them back.
162
+ attachLogFile.detach();
163
+ for (const restore of restores) restore();
164
+ if (cached) require.cache[resolved] = cached;
165
+ else delete require.cache[resolved];
166
+ }
167
+ },
168
+ },
169
+ {
170
+ // One pipeline, two environments (Ian on #865): the runner's publishing
171
+ // legs and this laptop's `--direct` run the SAME publish, so the three
172
+ // links between them are read from the files here. Nothing pinned them
173
+ // before, and the workflow step, the script and the verb could each move
174
+ // without the other two.
175
+ name: '--direct runs the ONE publish CI runs: the workflow\'s release:local script, local flag and all (#865)',
176
+ run: async (ctx) => {
177
+ // 1. What the runner runs, taken from the workflow the scaffold writes.
178
+ const legs = ciPublishLegs();
179
+
180
+ ctx.expect(legs).toEqual([
181
+ { os: 'macos-latest', command: 'npm run release:local' },
182
+ { os: 'ubuntu-latest', command: 'npm run release:local' },
183
+ ]);
184
+
185
+ // 2. Expanded through the target's own scripts, that command IS the
186
+ // publish verb under the local flag.
187
+ const script = SCRIPTS[legs[0].command.replace(/^npm run /, '')];
188
+
189
+ ctx.expect(script).toBe('omega publish --local');
190
+
191
+ // 3. And `--direct` calls the very same publish, in process.
192
+ const ran = [];
193
+ const published = [];
194
+ const restores = [
195
+ stubModule(path.join(COMMANDS, 'lib', 'ensure-target.js'), { ensureTarget: async () => ran.push('ensure-target') }),
196
+ stubModule(path.join(COMMANDS, 'lib', 'deploy-precheck.js'), { deployPrecheck: async () => ran.push('precheck') }),
197
+ stubModule(path.join(COMMANDS, 'publish.js'), async (options) => {
198
+ ran.push('publish');
199
+ published.push(options);
200
+ }),
201
+ ];
202
+ // A real run stamps the brand's deploy record, and a test must never
203
+ // write one into the tree it happens to be standing in.
204
+ const recordDeploy = deployRecord.recordDeploy;
205
+ deployRecord.recordDeploy = () => {};
206
+ const resolved = require.resolve(DEPLOY);
207
+ const cached = require.cache[resolved];
208
+ delete require.cache[resolved];
209
+
210
+ try {
211
+ const deploy = require(resolved);
212
+
213
+ // This machine's platform, in the vocabulary the flag speaks: the
214
+ // direct lane builds the host and only the host, whichever host is
215
+ // running the suite.
216
+ await deploy({ direct: true, platforms: directPlatform(undefined, process.platform) });
217
+
218
+ // The publish ran once, under the local flag, and the precheck (the
219
+ // network boundary) never did.
220
+ ctx.expect(ran).toEqual(['ensure-target', 'publish']);
221
+ ctx.expect(published.length).toBe(1);
222
+ ctx.expect(published[0].local).toBe(true);
223
+
224
+ // 4. The two ends meet at that flag. `src/cli-run.js` is what maps
225
+ // one to the other: it declares `local` boolean and hands the parsed
226
+ // options to the router, so the script's `--local` arrives at the
227
+ // verb as `{ local: true }`. The parse itself is pinned in
228
+ // build/cli.test.js; this pin names the flag on both sides.
229
+ ctx.expect(script.split(' ').filter((word) => word.startsWith('--'))).toEqual(['--local']);
230
+ } finally {
231
+ // The verb tees this process' writers now (#873): hand them back.
232
+ attachLogFile.detach();
233
+ deployRecord.recordDeploy = recordDeploy;
234
+ for (const restore of restores) restore();
235
+ if (cached) require.cache[resolved] = cached;
236
+ else delete require.cache[resolved];
237
+ }
238
+ },
239
+ },
240
+ ],
241
+ });
@@ -0,0 +1,287 @@
1
+ // Build-layer test for the dispatch lane's run follower
2
+ // ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)).
3
+ //
4
+ // `omega deploy` delegates to `omega release`, which owns desktop's dispatch.
5
+ // The dispatch used to BE the verb's answer: it printed the runs page and
6
+ // exited 0 while the run it started went red minutes later. Now the ONE devkit
7
+ // follower (`@omega.js/devkit/deploy-follow`, ported out of this very file) is
8
+ // called with the repo and workflow the dispatch used, and its verdict is the
9
+ // verb's exit code. The follower's own behavior is pinned in devkit's
10
+ // `test/deploy-follow.test.js`.
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
16
+
17
+ const RELEASE = path.join(__dirname, '..', '..', '..', 'commands', 'release.js');
18
+
19
+ // Required as literals: that is what resolves to the same instance the command
20
+ // loads (a vendored devkit inside dist, the workspace one from src).
21
+ const devkitDeploy = require('../../../vendor/devkit/deploy.js');
22
+ const devkitFollow = require('../../../vendor/devkit/deploy-follow.js');
23
+ const devkitRecord = require('../../../vendor/devkit/deploy-record.js');
24
+ const attachLogFile = require('../../../vendor/devkit/attach-log-file.js');
25
+
26
+ // The dispatch a stub stands in for: accepted, on the ONE lane (#915).
27
+ const LANE = { mode: 'snapshot', ref: 'omega-deploy' };
28
+
29
+ /**
30
+ * A desktop target inside a brand that names its repo, run as cwd.
31
+ *
32
+ * @returns {{ brandRoot: string, targetDir: string }} The staged tree.
33
+ */
34
+ function stageBrand() {
35
+ const brandRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-dispatch-')));
36
+ const targetDir = path.join(brandRoot, 'targets', 'desktop');
37
+
38
+ fs.mkdirSync(path.join(brandRoot, 'config'), { recursive: true });
39
+ fs.writeFileSync(path.join(brandRoot, 'config', 'omega.json5'), JSON.stringify({
40
+ brand: { id: 'acme', name: 'Acme', url: 'https://acme.test' },
41
+ repo: { provider: 'github', org: 'Acme-Org' },
42
+ targets: { desktop: { type: 'desktop' } },
43
+ }));
44
+ fs.writeFileSync(path.join(brandRoot, 'package.json'), '{"name":"acme-brand","version":"0.0.0"}');
45
+ fs.mkdirSync(targetDir, { recursive: true });
46
+ fs.writeFileSync(path.join(targetDir, 'package.json'), '{"name":"acme-desktop","version":"0.0.0"}');
47
+
48
+ return { brandRoot, targetDir };
49
+ }
50
+
51
+ /**
52
+ * Run the release verb from a staged target with the network steps swapped
53
+ * out. The verb DESTRUCTURES its imports at load, so it is dropped from the
54
+ * cache and re-required inside the patch.
55
+ *
56
+ * @param {object} follower - What `followRun` does for this run.
57
+ * @param {object} [options] - Extra verb options for this run (the fan-out's `--snapshot`).
58
+ * @param {object} [token] - The token surfaces this run has: `env` is what
59
+ * `GH_TOKEN` holds (null for a shell that never exported one) and `resolved`
60
+ * is what devkit's `resolveToken()` chain answers (null for a machine with no
61
+ * token anywhere, `gh` included).
62
+ * @returns {Promise<{ sent: object[], followed: object[], error: Error|null, lines: string[], targetDir: string, brandRoot: string }>} The run's record.
63
+ */
64
+ async function runRelease(follower, options = {}, token = {}) {
65
+ const envToken = 'env' in token ? token.env : 'tok';
66
+ const resolvedToken = 'resolved' in token ? token.resolved : 'tok';
67
+ const { brandRoot, targetDir } = stageBrand();
68
+ const followed = [];
69
+ const sent = [];
70
+ const patched = [
71
+ [devkitDeploy, 'deployViaDispatch', async (args) => {
72
+ sent.push(args);
73
+ // The executor's own answer shape (`buildDispatch`): the verb prints the
74
+ // plan's runs url rather than rebuilding that address for itself, and a
75
+ // run carrying the root's snapshot is on the snapshot lane by definition.
76
+ // The `sha` is part of that shape (#902): it is what the verb labels the
77
+ // dispatch with and what it holds the followed run's head to, so a stub
78
+ // that dropped it hid both.
79
+ return {
80
+ sha: args.snapshot || null,
81
+ plan: {
82
+ method: 'POST',
83
+ url: `https://api.github.com/repos/${args.owner}/${args.repo}/actions/workflows/${args.workflow}/dispatches`,
84
+ body: { ref: LANE.ref, ...(args.inputs ? { inputs: args.inputs } : {}) },
85
+ runsUrl: `https://github.com/${args.owner}/${args.repo}/actions/workflows/${args.workflow}`,
86
+ },
87
+ dispatched: !args.dryRun,
88
+ lane: LANE,
89
+ };
90
+ }],
91
+ [devkitDeploy, 'resolveToken', () => resolvedToken],
92
+ [devkitRecord, 'recordDeploy', () => {}],
93
+ [devkitFollow, 'followRun', async (args) => {
94
+ followed.push(args);
95
+ return follower(args);
96
+ }],
97
+ ];
98
+ const restore = patched.map(([module, key]) => [module, key, module[key]]);
99
+ for (const [module, key, value] of patched) module[key] = value;
100
+
101
+ const lines = [];
102
+ const previousLog = console.log;
103
+ console.log = (...args) => lines.push(args.map(String).join(' '));
104
+
105
+ const previousCwd = process.cwd();
106
+ const previousToken = process.env.GH_TOKEN;
107
+ if (envToken === null) delete process.env.GH_TOKEN;
108
+ else process.env.GH_TOKEN = envToken;
109
+ process.chdir(targetDir);
110
+ delete require.cache[require.resolve(RELEASE)];
111
+
112
+ let error = null;
113
+ try {
114
+ await require(RELEASE)(options);
115
+ } catch (e) {
116
+ error = e;
117
+ } finally {
118
+ // The verb tees this process' writers: hand them back before the next
119
+ // suite prints through a fixture that is about to be gone.
120
+ attachLogFile.detach();
121
+ console.log = previousLog;
122
+ process.chdir(previousCwd);
123
+ if (previousToken === undefined) delete process.env.GH_TOKEN;
124
+ else process.env.GH_TOKEN = previousToken;
125
+ for (const [module, key, value] of restore) module[key] = value;
126
+ delete require.cache[require.resolve(RELEASE)];
127
+ }
128
+
129
+ return { sent, followed, error, lines, targetDir, brandRoot };
130
+ }
131
+
132
+ module.exports = defineCases({
133
+ type: 'suite',
134
+ layer: 'build',
135
+ description: 'deploy dispatch: the run is followed, and its conclusion is the verb\'s (#873)',
136
+ tests: [
137
+ {
138
+ name: 'the dispatched run is followed with the repo and workflow the dispatch used (#873, #847)',
139
+ run: async (ctx) => {
140
+ const { sent, followed, error, targetDir, brandRoot } = await runRelease(async () => ({ run: {}, conclusion: 'success' }));
141
+
142
+ try {
143
+ ctx.expect(error).toBe(null);
144
+
145
+ // The dispatch address is devkit's ONE helper's (#847): the repo the
146
+ // brand's config names, and the per-target workflow the scaffold
147
+ // composed at the brand root (#265).
148
+ ctx.expect(sent.length).toBe(1);
149
+ ctx.expect({ owner: sent[0].owner, repo: sent[0].repo, workflow: sent[0].workflow })
150
+ .toEqual({ owner: 'Acme-Org', repo: 'acme-omega', workflow: 'desktop-build.yml' });
151
+ ctx.expect(followed.length).toBe(1);
152
+ ctx.expect(followed[0].owner).toBe('Acme-Org');
153
+ ctx.expect(followed[0].repo).toBe('acme-omega');
154
+ // The COMPOSED name (#265), the one the dispatch itself named.
155
+ ctx.expect(followed[0].workflow).toBe('desktop-build.yml');
156
+ ctx.expect(followed[0].token).toBe('tok');
157
+ ctx.expect(followed[0].since instanceof Date).toBe(true);
158
+ // And the whole verb went to the target's own deploy log, the name
159
+ // every target's deploy writes now (logs/ci.log is retired).
160
+ ctx.expect(fs.existsSync(path.join(targetDir, 'logs', 'deploy.log'))).toBe(true);
161
+ ctx.expect(fs.existsSync(path.join(targetDir, 'logs', 'ci.log'))).toBe(false);
162
+ } finally {
163
+ fs.rmSync(brandRoot, { recursive: true, force: true });
164
+ }
165
+ },
166
+ },
167
+ {
168
+ // #901: the brand root pushes the snapshot ONCE and hands every target
169
+ // of the run the sha, so this verb dispatches against it and pushes
170
+ // nothing. The flag is the root's word to the verb, never typed.
171
+ name: '--snapshot=<sha> reaches the executor as the run\'s snapshot (#901)',
172
+ run: async (ctx) => {
173
+ const { sent, followed, error, lines, brandRoot } = await runRelease(
174
+ async () => ({ run: {}, conclusion: 'success' }),
175
+ { snapshot: 'abc1234567890' },
176
+ );
177
+
178
+ try {
179
+ ctx.expect(error).toBe(null);
180
+ ctx.expect(sent.length).toBe(1);
181
+ ctx.expect(sent[0].snapshot).toBe('abc1234567890');
182
+
183
+ // And the sha the executor answers with is what the run is BOTH
184
+ // labelled with and held to (#902): the dispatch line names it, and
185
+ // the follower refuses a run whose head is anything else.
186
+ ctx.expect(lines.join('\n')).toContain('Dispatched desktop-build.yml (snapshot lane, ref omega-deploy @ abc1234)');
187
+ ctx.expect(followed.length).toBe(1);
188
+ ctx.expect(followed[0].headSha).toBe('abc1234567890');
189
+ } finally {
190
+ fs.rmSync(brandRoot, { recursive: true, force: true });
191
+ }
192
+ },
193
+ },
194
+ {
195
+ // The token chain is devkit's ONE resolver, `GH_TOKEN` → `GITHUB_TOKEN` →
196
+ // `gh auth token`, which every other target and this verb's own follower
197
+ // already use. Testing the bare env var here refused a machine signed in
198
+ // with `gh` and holding no variable at all.
199
+ name: 'a signed-in `gh` and no GH_TOKEN in the env still dispatches: the token chain is the one resolver',
200
+ run: async (ctx) => {
201
+ const { sent, followed, error, brandRoot } = await runRelease(
202
+ async () => ({ run: {}, conclusion: 'success' }),
203
+ {},
204
+ { env: null, resolved: 'gh-cli-token' },
205
+ );
206
+
207
+ try {
208
+ ctx.expect(error).toBe(null);
209
+ ctx.expect(sent.length).toBe(1);
210
+ ctx.expect(followed[0].token).toBe('gh-cli-token');
211
+ } finally {
212
+ fs.rmSync(brandRoot, { recursive: true, force: true });
213
+ }
214
+ },
215
+ },
216
+ {
217
+ name: 'no token anywhere refuses by name, before the dispatch',
218
+ run: async (ctx) => {
219
+ const { sent, error, brandRoot, targetDir } = await runRelease(
220
+ async () => ({ run: {}, conclusion: 'success' }),
221
+ {},
222
+ { env: null, resolved: null },
223
+ );
224
+
225
+ try {
226
+ ctx.expect(error === null).toBe(false);
227
+ ctx.expect(error.message).toContain('GH_TOKEN');
228
+ ctx.expect(error.message).toContain('gh auth login');
229
+ ctx.expect(sent.length).toBe(0);
230
+ // The tee attached before the token check (#873), so the harness's
231
+ // detach above popped this verb's layer and not the `omega test` log
232
+ // under it.
233
+ ctx.expect(fs.existsSync(path.join(targetDir, 'logs', 'deploy.log'))).toBe(true);
234
+ } finally {
235
+ fs.rmSync(brandRoot, { recursive: true, force: true });
236
+ }
237
+ },
238
+ },
239
+ {
240
+ // P4/P6: the preview used to be a SECOND dispatch call in deploy.js, built
241
+ // without the snapshot or the ref, so `--dry-run --snapshot=<sha>`
242
+ // printed a plan that ignored the sha the fan-out pushed. One call site
243
+ // now answers both.
244
+ name: 'a --dry-run threads the snapshot and sends nothing, and the runs url is the PLAN\'s (#895, #901)',
245
+ run: async (ctx) => {
246
+ const { sent, followed, error, lines, brandRoot } = await runRelease(
247
+ async () => ({ run: {}, conclusion: 'success' }),
248
+ { dryRun: true, snapshot: 'abc1234567890', platforms: 'mac' },
249
+ // A dry run resolves no token: every step under it is a read or a plan.
250
+ { env: null, resolved: null },
251
+ );
252
+
253
+ try {
254
+ ctx.expect(error).toBe(null);
255
+ ctx.expect(sent.length).toBe(1);
256
+ ctx.expect(sent[0].dryRun).toBe(true);
257
+ ctx.expect(sent[0].snapshot).toBe('abc1234567890');
258
+ ctx.expect(followed.length).toBe(0);
259
+
260
+ const preview = lines.join('\n');
261
+ // The header names the LANE, the same spelling the other three verbs
262
+ // print (docs/shared/deploys.md); the sha rides the Dispatched line only.
263
+ ctx.expect(preview).toContain('DRY RUN (snapshot lane, ref omega-deploy)');
264
+ ctx.expect(preview).not.toContain('abc1234');
265
+ ctx.expect(preview).toContain('https://github.com/Acme-Org/acme-omega/actions/workflows/desktop-build.yml');
266
+ } finally {
267
+ fs.rmSync(brandRoot, { recursive: true, force: true });
268
+ }
269
+ },
270
+ },
271
+ {
272
+ name: 'a follower that throws is the verb throwing, so a red run exits 1 (#873)',
273
+ run: async (ctx) => {
274
+ const { error, brandRoot } = await runRelease(async () => {
275
+ throw new Error('desktop-build.yml concluded failure: https://github.com/Acme-Org/acme-omega/actions/runs/7');
276
+ });
277
+
278
+ try {
279
+ ctx.expect(error && error.message).toContain('concluded failure');
280
+ ctx.expect(error.message).toContain('actions/runs/7');
281
+ } finally {
282
+ fs.rmSync(brandRoot, { recursive: true, force: true });
283
+ }
284
+ },
285
+ },
286
+ ],
287
+ });