@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,135 @@
1
+ // The DMG hook: artifactBuildCompleted notarizes, staples and PROVES each disk
2
+ // image BEFORE electron-builder uploads it
3
+ // ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
4
+ //
5
+ // The ticket the afterSign hook staples to the .app rides INSIDE the image, so
6
+ // a downloaded .dmg is assessed on its own and an unstapled one is refused on an
7
+ // offline Mac. And the hook has to run per artifact: `afterAllArtifactBuild`
8
+ // fires after every upload is queued, so run 34738410986 published a signed
9
+ // and notarized image that carried no staple. Offline by construction here:
10
+ // `xcrun` and `spctl` are a fake runner injected through the hook's own option,
11
+ // never a global.
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
17
+
18
+ const notarizeArtifacts = require(path.join(__dirname, '..', '..', '..', 'hooks', 'notarize-artifacts.js'));
19
+
20
+ const ACCEPTED = {
21
+ 'notarytool submit': ' status: Accepted\n message: Successfully received submission info',
22
+ 'stapler staple': 'The staple and validate action worked!',
23
+ 'stapler validate': 'The validate action worked!',
24
+ 'spctl --assess': '/tmp/App.dmg: accepted\nsource=Notarized Developer ID',
25
+ };
26
+
27
+ function fakeTools(overrides = {}) {
28
+ const commands = [];
29
+ const answers = { ...ACCEPTED, ...overrides };
30
+
31
+ const run = async (command) => {
32
+ commands.push(command);
33
+ const key = Object.keys(answers).find((needle) => command.includes(needle));
34
+ const answer = key ? answers[key] : '';
35
+ if (answer instanceof Error) throw answer;
36
+ return answer;
37
+ };
38
+
39
+ return { run, commands };
40
+ }
41
+
42
+ // The credentials the hook needs, restored after each case.
43
+ function withCredentials(body) {
44
+ const prior = {
45
+ APPLE_API_KEY: process.env.APPLE_API_KEY,
46
+ APPLE_API_KEY_ID: process.env.APPLE_API_KEY_ID,
47
+ APPLE_API_ISSUER: process.env.APPLE_API_ISSUER,
48
+ };
49
+ const keyFile = path.join(os.tmpdir(), 'AuthKey_ARTIFACTS_TEST.p8');
50
+ fs.writeFileSync(keyFile, 'fixture-key');
51
+ process.env.APPLE_API_KEY = keyFile;
52
+ process.env.APPLE_API_KEY_ID = 'TESTKEYID1';
53
+ process.env.APPLE_API_ISSUER = 'issuer-uuid';
54
+
55
+ const restore = () => {
56
+ fs.rmSync(keyFile, { force: true });
57
+ for (const [name, value] of Object.entries(prior)) {
58
+ if (value) process.env[name] = value;
59
+ else delete process.env[name];
60
+ }
61
+ };
62
+
63
+ return Promise.resolve(body(keyFile)).finally(restore);
64
+ }
65
+
66
+ module.exports = defineCases({
67
+ type: 'suite',
68
+ layer: 'build',
69
+ description: 'notarize-artifacts: every DMG is notarized, stapled and proved before its upload (#891)',
70
+ tests: [
71
+ {
72
+ name: 'a .dmg artifact is submitted, stapled, validated and assessed; other artifacts are ignored',
73
+ run: (ctx) => withCredentials(async (keyFile) => {
74
+ const { run, commands } = fakeTools();
75
+
76
+ // electron-builder calls the hook once per artifact, with that
77
+ // artifact's `file`, before it queues the upload.
78
+ for (const file of ['/tmp/App-1.0.0.dmg', '/tmp/App-1.0.0-mac.zip', '/tmp/latest-mac.yml']) {
79
+ await notarizeArtifacts({ file }, { run });
80
+ }
81
+
82
+ ctx.expect(commands.length).toBe(4);
83
+ ctx.expect(commands[0]).toContain('xcrun notarytool submit "/tmp/App-1.0.0.dmg"');
84
+ ctx.expect(commands[0]).toContain(`--key "${keyFile}" --key-id "TESTKEYID1" --issuer "issuer-uuid" --wait`);
85
+ ctx.expect(commands[1]).toContain('xcrun stapler staple "/tmp/App-1.0.0.dmg"');
86
+ ctx.expect(commands[2]).toContain('xcrun stapler validate "/tmp/App-1.0.0.dmg"');
87
+ ctx.expect(commands[3]).toContain('spctl --assess --type open --context context:primary-signature -vv "/tmp/App-1.0.0.dmg"');
88
+ // The .zip and the update feed are never submitted
89
+ ctx.expect(commands.some((command) => command.includes('.zip') || command.includes('.yml'))).toBe(false);
90
+ }),
91
+ },
92
+ {
93
+ name: 'a non-DMG artifact is a no-op: the linux and windows legs never reach a tool',
94
+ run: async (ctx) => {
95
+ const { run, commands } = fakeTools();
96
+
97
+ await notarizeArtifacts({ file: '/tmp/App.AppImage' }, { run });
98
+ await notarizeArtifacts({ file: '/tmp/App.exe' }, { run });
99
+ ctx.expect(commands).toEqual([]);
100
+ },
101
+ },
102
+ {
103
+ name: 'a rejected submission throws, so the artifact is never uploaded',
104
+ run: (ctx) => withCredentials(async () => {
105
+ const invalid = fakeTools({ 'notarytool submit': ' status: Invalid\n message: Package Invalid' });
106
+ await ctx.expect(() => notarizeArtifacts({ file: '/tmp/App.dmg' }, { run: invalid.run }))
107
+ .toThrow(/notarization of \/tmp\/App\.dmg was not accepted/);
108
+ // Nothing was stapled onto an artifact Apple refused
109
+ ctx.expect(invalid.commands.length).toBe(1);
110
+
111
+ const refused = fakeTools({ 'spctl --assess': new Error('/tmp/App.dmg: rejected') });
112
+ await ctx.expect(() => notarizeArtifacts({ file: '/tmp/App.dmg' }, { run: refused.run }))
113
+ .toThrow(/Gatekeeper refused/);
114
+ }),
115
+ },
116
+ {
117
+ name: 'a DMG with no APPLE_* credentials throws instead of shipping unnotarized',
118
+ run: async (ctx) => {
119
+ const prior = { key: process.env.APPLE_API_KEY, id: process.env.APPLE_API_KEY_ID, issuer: process.env.APPLE_API_ISSUER };
120
+ delete process.env.APPLE_API_KEY;
121
+ delete process.env.APPLE_API_KEY_ID;
122
+ delete process.env.APPLE_API_ISSUER;
123
+
124
+ try {
125
+ const { run } = fakeTools();
126
+ await ctx.expect(() => notarizeArtifacts({ file: '/tmp/App.dmg' }, { run })).toThrow(/APPLE_API_KEY/);
127
+ } finally {
128
+ if (prior.key) process.env.APPLE_API_KEY = prior.key;
129
+ if (prior.id) process.env.APPLE_API_KEY_ID = prior.id;
130
+ if (prior.issuer) process.env.APPLE_API_ISSUER = prior.issuer;
131
+ }
132
+ },
133
+ },
134
+ ],
135
+ });
@@ -0,0 +1,38 @@
1
+ // The notarization tools' command runner
2
+ // ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
3
+ //
4
+ // notarytool, stapler and spctl write their verdict to the console and exit
5
+ // non-zero when they refuse, so the runner must hand back what they SAID no
6
+ // matter how they exited. Run 34735658588's DMG assessment failed with only
7
+ // `Command failed with exit code 3`: the verdict itself was lost.
8
+
9
+ const path = require('path');
10
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
11
+
12
+ const { report, toolRunner } = require(path.join(__dirname, '..', '..', '..', 'hooks', 'lib', 'notarize-tools.js'));
13
+
14
+ module.exports = defineCases({
15
+ type: 'suite',
16
+ layer: 'build',
17
+ description: 'notarize-tools: the runner reports a refusing tool\'s own words (#891)',
18
+ tests: [
19
+ {
20
+ name: 'a command that exits non-zero still reports its stdout and stderr',
21
+ run: async (ctx) => {
22
+ const output = await report(toolRunner(), 'echo "App.dmg: rejected"; echo "source=Insufficient Context" 1>&2; exit 3');
23
+
24
+ ctx.expect(output).toContain('App.dmg: rejected');
25
+ ctx.expect(output).toContain('source=Insufficient Context');
26
+ },
27
+ },
28
+ {
29
+ name: 'a command that exits zero reports its output the same way',
30
+ run: async (ctx) => {
31
+ const output = await report(toolRunner(), 'echo "App.dmg: accepted"; echo "source=Notarized Developer ID" 1>&2');
32
+
33
+ ctx.expect(output).toContain('App.dmg: accepted');
34
+ ctx.expect(output).toContain('source=Notarized Developer ID');
35
+ },
36
+ },
37
+ ],
38
+ });
@@ -0,0 +1,207 @@
1
+ // The notarize hook's Developer ID gate
2
+ // ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
3
+ //
4
+ // electron-builder SKIPS mac code signing when it finds no identity ("cannot find
5
+ // valid Developer ID Application identity ... 0 identities found") and ships the
6
+ // ad-hoc signature the linker left. The hook then notarized it anyway, because
7
+ // APPLE_API_KEY was set, and notarytool rejected the submission: "Failed to
8
+ // codesign your application with code: 1" ended the whole mac leg of a build that
9
+ // had otherwise succeeded.
10
+ //
11
+ // The seam is codesign's OWN report: a pure predicate over its text, plus an
12
+ // injectable exec for reading it. Both fixtures below are the verbatim output of
13
+ // run 34584322778 and of a signed build.
14
+
15
+ const path = require('path');
16
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
17
+
18
+ const notarizeHook = require(path.join(__dirname, '..', '..', '..', 'hooks', 'notarize.js'));
19
+ const { isDeveloperIdSigned, readCodesign } = notarizeHook;
20
+ const { stapleAndProve } = require(path.join(__dirname, '..', '..', '..', 'hooks', 'lib', 'notarize-tools.js'));
21
+
22
+ // What the real tools print when they are happy. A fake `run` answers by
23
+ // command, so the SEQUENCE and the refusal are both provable offline.
24
+ const TOOL_OUTPUT = {
25
+ 'stapler staple': 'Processing: /tmp/App.app\nThe staple and validate action worked!',
26
+ 'stapler validate': 'Processing: /tmp/App.app\nThe validate action worked!',
27
+ 'spctl --assess': '/tmp/App.app: accepted\nsource=Notarized Developer ID',
28
+ 'notarytool submit': ' status: Accepted\n message: Successfully received submission info',
29
+ };
30
+
31
+ function fakeTools(overrides = {}) {
32
+ const commands = [];
33
+ const answers = { ...TOOL_OUTPUT, ...overrides };
34
+
35
+ const run = async (command) => {
36
+ commands.push(command);
37
+ const key = Object.keys(answers).find((needle) => command.includes(needle));
38
+ const answer = key ? answers[key] : '';
39
+ if (answer instanceof Error) throw answer;
40
+ return answer;
41
+ };
42
+
43
+ return { run, commands };
44
+ }
45
+
46
+ // What the runner's own `codesign -dv --verbose=2` printed for the unsigned app.
47
+ const ADHOC = `Executable=/Users/runner/work/playground-omega/targets/desktop/release/mac-universal/OMEGA Playground.app/Contents/MacOS/OMEGA Playground
48
+ Identifier=Electron
49
+ Format=app bundle with Mach-O universal (x86_64 arm64)
50
+ CodeDirectory v=20400 size=392 flags=0x20002(adhoc,linker-signed) hashes=9+0 location=embedded
51
+ Signature=adhoc
52
+ Info.plist=not bound
53
+ TeamIdentifier=not set
54
+ Sealed Resources=none`;
55
+
56
+ // The same report for a build that DID find the identity.
57
+ const SIGNED = `Executable=/Users/runner/work/app/release/mac-universal/App.app/Contents/MacOS/App
58
+ Identifier=dev.omegajs.app
59
+ Format=app bundle with Mach-O universal (x86_64 arm64)
60
+ CodeDirectory v=20500 size=1234 flags=0x10000(runtime) hashes=30+7 location=embedded
61
+ Signature size=9000
62
+ Authority=Developer ID Application: ITW Creative Works (ABCDE12345)
63
+ Authority=Developer ID Certification Authority
64
+ Authority=Apple Root CA
65
+ TeamIdentifier=ABCDE12345
66
+ Sealed Resources version=2 rules=13 files=40`;
67
+
68
+ // An app with no signature at all: codesign exits NON-ZERO and says so.
69
+ const UNSIGNED_ERROR = 'OMEGA Playground.app: code object is not signed at all';
70
+
71
+ module.exports = defineCases({
72
+ type: 'suite',
73
+ layer: 'build',
74
+ description: 'notarize: only a Developer ID signed app is submitted (#872), and the staple is PROVED (#891)',
75
+ tests: [
76
+ {
77
+ name: 'an ad-hoc signature is not a Developer ID signature',
78
+ run: (ctx) => {
79
+ ctx.expect(isDeveloperIdSigned(ADHOC)).toBe(false);
80
+ ctx.expect(isDeveloperIdSigned(UNSIGNED_ERROR)).toBe(false);
81
+ ctx.expect(isDeveloperIdSigned('')).toBe(false);
82
+ ctx.expect(isDeveloperIdSigned(undefined)).toBe(false);
83
+ },
84
+ },
85
+ {
86
+ name: 'a Developer ID Application authority is',
87
+ run: (ctx) => {
88
+ ctx.expect(isDeveloperIdSigned(SIGNED)).toBe(true);
89
+
90
+ // A signature from some OTHER authority cannot be notarized either.
91
+ ctx.expect(isDeveloperIdSigned(SIGNED.replace('Authority=Developer ID Application:', 'Authority=Apple Development:'))).toBe(false);
92
+ },
93
+ },
94
+ {
95
+ name: 'the report is read with stderr folded in, and a failing codesign is the answer',
96
+ run: async (ctx) => {
97
+ const commands = [];
98
+ const report = await readCodesign('/tmp/App.app', (cmd) => {
99
+ commands.push(cmd);
100
+ return Promise.resolve(SIGNED);
101
+ });
102
+
103
+ // codesign writes its report to STDERR and exits 0, so a plain stdout
104
+ // read comes back EMPTY and every app would look unsigned.
105
+ ctx.expect(commands[0]).toContain('2>&1');
106
+ ctx.expect(commands[0]).toContain('/tmp/App.app');
107
+ ctx.expect(isDeveloperIdSigned(report)).toBe(true);
108
+
109
+ // An unsigned app exits non-zero; the report rides in the rejection.
110
+ const failed = await readCodesign('/tmp/App.app', () => Promise.reject(new Error(UNSIGNED_ERROR)));
111
+ ctx.expect(failed).toContain('not signed at all');
112
+ ctx.expect(isDeveloperIdSigned(failed)).toBe(false);
113
+ },
114
+ },
115
+ // ── The staple and its PROOF (#891) ─────────────────────────────────────
116
+ // Proof run one published an app no other Mac would open. Notarizing is
117
+ // half the job: the ticket has to be stapled, and the staple has to be
118
+ // read back, or an offline Mac still refuses.
119
+ {
120
+ name: 'stapling runs staple → validate → spctl, in that order, on the .app',
121
+ run: async (ctx) => {
122
+ const { run, commands } = fakeTools();
123
+
124
+ await stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run });
125
+
126
+ ctx.expect(commands.length).toBe(3);
127
+ ctx.expect(commands[0]).toContain('xcrun stapler staple "/tmp/App.app"');
128
+ ctx.expect(commands[1]).toContain('xcrun stapler validate "/tmp/App.app"');
129
+ ctx.expect(commands[2]).toContain('spctl --assess --type execute -vv "/tmp/App.app"');
130
+ // The verdict these tools write to stderr is the RUNNER's to capture
131
+ // (notarize-tools.test.js), never a shell fold on the command.
132
+ },
133
+ },
134
+ {
135
+ name: 'a DMG is assessed as an OPENED image against its own signature',
136
+ run: async (ctx) => {
137
+ const { run, commands } = fakeTools({ 'spctl --assess': '/tmp/App.dmg: accepted\nsource=Notarized Developer ID' });
138
+
139
+ await stapleAndProve({ filePath: '/tmp/App.dmg', kind: 'dmg', run });
140
+
141
+ ctx.expect(commands[2]).toContain('spctl --assess --type open --context context:primary-signature -vv "/tmp/App.dmg"');
142
+ },
143
+ },
144
+ {
145
+ name: 'a REJECTED assessment throws: the release step never runs',
146
+ run: async (ctx) => {
147
+ // spctl exits non-zero and says so on stderr
148
+ const rejected = fakeTools({ 'spctl --assess': new Error('/tmp/App.app: rejected\nsource=no usable signature') });
149
+ await ctx.expect(() => stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run: rejected.run })).toThrow(/Gatekeeper refused/);
150
+
151
+ // ...and so does a staple that never took, or a ticket that does not read back
152
+ const unstapled = fakeTools({ 'stapler staple': new Error('The staple and validate action failed! Error 65.') });
153
+ await ctx.expect(() => stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run: unstapled.run })).toThrow(/could not staple/);
154
+
155
+ const unreadable = fakeTools({ 'stapler validate': new Error('The validate action failed! Error 65.') });
156
+ await ctx.expect(() => stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run: unreadable.run })).toThrow(/carries no valid notarization ticket/);
157
+ },
158
+ },
159
+ // ── The hook's own refusals (#891) ──────────────────────────────────────
160
+ // Both of these used to be `logger.warn` + `return`, and BOTH of them were
161
+ // hit on the run that shipped an unsigned app.
162
+ {
163
+ name: 'a mac build with no APPLE_* credentials THROWS instead of skipping',
164
+ run: async (ctx) => {
165
+ const prior = { key: process.env.APPLE_API_KEY, id: process.env.APPLE_API_KEY_ID, issuer: process.env.APPLE_API_ISSUER };
166
+ delete process.env.APPLE_API_KEY;
167
+ delete process.env.APPLE_API_KEY_ID;
168
+ delete process.env.APPLE_API_ISSUER;
169
+
170
+ try {
171
+ await ctx.expect(() => notarizeHook({ electronPlatformName: 'darwin', appOutDir: '/tmp' })).toThrow(/APPLE_API_KEY/);
172
+ // A non-mac leg is still the ONE legitimate skip
173
+ ctx.expect(await notarizeHook({ electronPlatformName: 'win32', appOutDir: '/tmp' })).toBe(undefined);
174
+ } finally {
175
+ if (prior.key) process.env.APPLE_API_KEY = prior.key;
176
+ if (prior.id) process.env.APPLE_API_KEY_ID = prior.id;
177
+ if (prior.issuer) process.env.APPLE_API_ISSUER = prior.issuer;
178
+ }
179
+ },
180
+ },
181
+ {
182
+ name: 'an ad-hoc signed .app THROWS instead of publishing unnotarized',
183
+ run: async (ctx) => {
184
+ const prior = { key: process.env.APPLE_API_KEY, id: process.env.APPLE_API_KEY_ID, issuer: process.env.APPLE_API_ISSUER };
185
+ const keyFile = path.join(require('os').tmpdir(), `AuthKey_NOTARIZE_TEST.p8`);
186
+ require('fs').writeFileSync(keyFile, 'fixture-key');
187
+ process.env.APPLE_API_KEY = keyFile;
188
+ process.env.APPLE_API_KEY_ID = 'TESTKEYID1';
189
+ process.env.APPLE_API_ISSUER = 'issuer-uuid';
190
+
191
+ try {
192
+ const { run } = fakeTools({ 'codesign -dv': ADHOC });
193
+ await ctx.expect(() => notarizeHook(
194
+ { electronPlatformName: 'darwin', appOutDir: '/tmp', packager: { appInfo: { productFilename: 'App' } } },
195
+ { run },
196
+ )).toThrow(/is NOT Developer ID signed/);
197
+ } finally {
198
+ require('fs').rmSync(keyFile, { force: true });
199
+ for (const [name, value] of [['APPLE_API_KEY', prior.key], ['APPLE_API_KEY_ID', prior.id], ['APPLE_API_ISSUER', prior.issuer]]) {
200
+ if (value) process.env[name] = value;
201
+ else delete process.env[name];
202
+ }
203
+ }
204
+ },
205
+ },
206
+ ],
207
+ });
@@ -8,7 +8,7 @@ const os = require('os');
8
8
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
9
9
 
10
10
  // Helper: stage a consumer dir with config/omega.json5 containing the given
11
- // platforms.win.signing.strategy (under targets.desktop in the raw file). Returns the
11
+ // platforms.windows.signing.strategy (under targets.desktop in the raw file). Returns the
12
12
  // abs path to the temp dir; caller is responsible for chdir'ing into it and cleaning up.
13
13
  function stageStrategyConfig({ strategy, cloudProvider }) {
14
14
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-sign-'));
@@ -17,7 +17,7 @@ function stageStrategyConfig({ strategy, cloudProvider }) {
17
17
  if (cloudProvider) signing.cloud = { provider: cloudProvider };
18
18
  fs.writeFileSync(
19
19
  path.join(tmp, 'config', 'omega.json5'),
20
- JSON.stringify({ targets: { desktop: { platforms: { win: { signing } } } } }),
20
+ JSON.stringify({ targets: { desktop: { type: 'desktop', platforms: { windows: { signing } } } } }),
21
21
  );
22
22
  return tmp;
23
23
  }
@@ -39,74 +39,51 @@ module.exports = defineCases({
39
39
  layer: 'build',
40
40
  description: 'release pipeline — package / release / sign-windows / notarize hook',
41
41
  tests: [
42
- {
43
- name: 'release: the dispatch address is the CONFIG\'s brand repo and the COMPOSED workflow name (#799)',
44
- run: (ctx) => {
45
- const { dispatchTarget } = require(path.join(__dirname, '..', '..', '..', 'commands', 'release.js'));
46
- const { brandRoot, targetDir } = stageBrand({ brand: { id: 'acme' }, repo: { providers: { github: { org: 'Acme-Org' } } } });
47
42
 
48
- try {
49
- // A target nested in a brand (and the brand nested in another repo, as
50
- // the playground is here) dispatches on the repo its CONFIG names: the
51
- // enclosing git remote is somebody else's repo entirely.
52
- // The name is the derived `<brand.id>-omega` default (#809): the config
53
- // declares an org and no repo of its own.
54
- ctx.expect(dispatchTarget({
55
- projectRoot: targetDir,
56
- config: { brand: { id: 'acme' }, repo: { providers: { github: { org: 'Acme-Org' } } } },
57
- })).toEqual({ owner: 'Acme-Org', repo: 'acme-omega', workflow: 'desktop-build.yml' });
58
- } finally {
59
- fs.rmSync(brandRoot, { recursive: true, force: true });
60
- }
61
- },
62
- },
63
43
  {
64
- name: 'release: a STANDALONE target dispatches the plain build.yml, and an unaddressable repo throws',
65
- run: (ctx) => {
66
- const { dispatchTarget } = require(path.join(__dirname, '..', '..', '..', 'commands', 'release.js'));
67
- const standalone = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-standalone-'));
44
+ name: 'deploy-precheck: provisions <org>/<brand.id>-releases PUBLIC through the devkit ensureRepo (#799, #883)',
45
+ run: async (ctx) => {
46
+ const { provisionReleaseRepos } = require(path.join(__dirname, '..', '..', '..', 'commands', 'lib', 'deploy-precheck.js'));
47
+ const calls = [];
48
+ const lines = [];
68
49
 
69
- try {
70
- const config = { brand: { id: 'acme' }, repo: { providers: { github: { org: 'Acme-Org', repo: 'itw-creative-works/acme-app' } } } };
71
- ctx.expect(dispatchTarget({ projectRoot: standalone, config })).toEqual({
72
- owner: 'itw-creative-works', repo: 'acme-app', workflow: 'build.yml',
73
- });
50
+ await provisionReleaseRepos({
51
+ log: (line) => lines.push(line),
52
+ warn: (line) => lines.push(line),
53
+ config: { brand: { id: 'omega-playground' }, repo: { provider: 'github', org: 'Omega-JS-Stack' }, releases: {} },
54
+ // The ONE github boundary (#883): `gh` with an argv array, so the
55
+ // create is readable here exactly as it runs.
56
+ execFn: (file, args) => {
57
+ calls.push({ file, args });
58
+ if (args[0] === 'api') throw new Error('gh api failed: 404 Not Found');
59
+ return '';
60
+ },
61
+ });
74
62
 
75
- // Half an address addresses nothing: fail loudly instead of POSTing to
76
- // `undefined/acme`.
77
- ctx.expect(() => dispatchTarget({ projectRoot: standalone, config: { brand: { id: 'acme' } } }))
78
- .toThrow(/brand repo/);
79
- } finally {
80
- fs.rmSync(standalone, { recursive: true, force: true });
81
- }
63
+ const create = calls.find((call) => call.args[0] === 'repo');
64
+ ctx.expect(create.file).toBe('gh');
65
+ ctx.expect(create.args.slice(0, 4)).toEqual(['repo', 'create', 'Omega-JS-Stack/omega-playground-releases', '--public']);
66
+ // A release needs a tag and a tag needs a commit: this is the one repo
67
+ // that is created WITH one.
68
+ ctx.expect(create.args).toContain('--add-readme');
69
+ ctx.expect(lines.join('\n')).toContain('Omega-JS-Stack/omega-playground-releases');
82
70
  },
83
71
  },
72
+
84
73
  {
85
- name: 'deploy-precheck: provisions <owner>/<brand.id>-releases from config, never a discovered owner (#799)',
74
+ name: 'deploy-precheck: a config that names no org warns and creates nothing (#883)',
86
75
  run: async (ctx) => {
87
76
  const { provisionReleaseRepos } = require(path.join(__dirname, '..', '..', '..', 'commands', 'lib', 'deploy-precheck.js'));
88
- const created = [];
89
- const octokit = {
90
- rest: {
91
- repos: {
92
- get: async () => { const error = new Error('Not Found'); error.status = 404; throw error; },
93
- createInOrg: async (args) => created.push(`${args.org}/${args.name}`),
94
- createForAuthenticatedUser: async (args) => created.push(`me/${args.name}`),
95
- },
96
- users: { getAuthenticated: async () => ({ data: { login: 'somebody-else' } }) },
97
- },
98
- };
99
77
  const lines = [];
100
78
 
101
79
  await provisionReleaseRepos({
102
80
  log: (line) => lines.push(line),
103
81
  warn: (line) => lines.push(line),
104
- octokit,
105
- config: { brand: { id: 'omega-playground' }, repo: { providers: { github: { org: 'Omega-JS-Stack' } } }, releases: {} },
82
+ config: { brand: { id: 'acme' }, releases: {} },
83
+ execFn: () => { throw new Error('gh must not run'); },
106
84
  });
107
85
 
108
- ctx.expect(created).toEqual(['Omega-JS-Stack/omega-playground-releases']);
109
- ctx.expect(lines.join('\n')).toContain('Omega-JS-Stack/omega-playground-releases');
86
+ ctx.expect(lines.join('\n')).toContain('repo.org');
110
87
  },
111
88
  },
112
89
  {
@@ -115,8 +92,8 @@ module.exports = defineCases({
115
92
  const finalizeRelease = require(path.join(__dirname, '..', '..', '..', 'commands', 'finalize-release.js'));
116
93
  const { brandRoot, targetDir } = stageBrand({
117
94
  brand: { id: 'acme' },
118
- repo: { providers: { github: { org: 'Acme-Org' } } },
119
- targets: { desktop: { releases: {} } },
95
+ repo: { provider: 'github', org: 'Acme-Org' },
96
+ targets: { desktop: { type: 'desktop', releases: {} } },
120
97
  });
121
98
  fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify({ name: 'acme-desktop', version: '1.2.3' }));
122
99
 
@@ -333,7 +310,9 @@ module.exports = defineCases({
333
310
  },
334
311
  },
335
312
  {
336
- name: 'notarize hook: warns + returns when API key env vars are missing',
313
+ // Flipped by #891: a mac build that reaches this hook is one this
314
+ // framework ships, so credentials it cannot find stop the build.
315
+ name: 'notarize hook: THROWS when the API key env vars are missing',
337
316
  run: async (ctx) => {
338
317
  const notarize = require(path.join(__dirname, '..', '..', '..', 'hooks', 'notarize.js'));
339
318
  // Snapshot + clear env.
@@ -347,12 +326,11 @@ module.exports = defineCases({
347
326
  delete process.env.APPLE_API_ISSUER;
348
327
 
349
328
  try {
350
- const result = await notarize({
329
+ await ctx.expect(() => notarize({
351
330
  electronPlatformName: 'darwin',
352
331
  appOutDir: '/tmp',
353
332
  packager: { appInfo: { productFilename: 'test' } },
354
- });
355
- ctx.expect(result).toBeUndefined();
333
+ })).toThrow(/APPLE_API_KEY, APPLE_API_KEY_ID and APPLE_API_ISSUER are required/);
356
334
  } finally {
357
335
  for (const [k, v] of Object.entries(snapshot)) {
358
336
  if (v !== undefined) process.env[k] = v;
@@ -0,0 +1,107 @@
1
+ // The release task's verdict on a build that uploaded NOTHING
2
+ // ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
3
+ //
4
+ // electron-builder's `build()` promise resolves with the LOCAL artifact paths
5
+ // whether or not a byte reached the publish provider, so every leg of run
6
+ // 34729947722 printed `Released 4 artifact(s)` while electron-publish had
7
+ // logged `skipped publishing` for each of them
8
+ // (`reason=existing release published more than 2 hours ago tag=v0.0.1`). A
9
+ // release that uploaded nothing is a failed release, and the only report of the
10
+ // skip is electron-builder's own logger, so the task reads it there.
11
+
12
+ const path = require('path');
13
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
14
+
15
+ const RELEASE_TASK = path.join(__dirname, '..', '..', '..', 'gulp', 'tasks', 'release.js');
16
+
17
+ // The fields electron-publish's gitHubPublisher carries on both warnings.
18
+ const PUBLISH_FIELDS = { tag: 'v0.0.1', reason: 'existing release published more than 2 hours ago' };
19
+
20
+ // A stand-in for builder-util's `log`: a Logger whose `warn(fields, message)`
21
+ // takes the two shapes the real one does, recording what it was handed.
22
+ function fakeBuilderLog() {
23
+ const seen = [];
24
+ return {
25
+ seen,
26
+ warn(messageOrFields, message) {
27
+ seen.push(message === undefined ? messageOrFields : message);
28
+ },
29
+ };
30
+ }
31
+
32
+ module.exports = defineCases({
33
+ type: 'suite',
34
+ layer: 'build',
35
+ description: 'release task (#891): a skipped upload is a failed release, never a "Released N artifact(s)" line',
36
+ tests: [
37
+ {
38
+ name: 'every skipped publish is recorded off the build logger, with its reason and tag',
39
+ run: async (ctx) => {
40
+ const { recordSkippedPublishes } = require(RELEASE_TASK);
41
+ const log = fakeBuilderLog();
42
+ const original = log.warn;
43
+
44
+ const { result, skips } = await recordSkippedPublishes(log, async () => {
45
+ log.warn(PUBLISH_FIELDS, 'GitHub release not created');
46
+ log.warn({ file: 'App-0.0.1.dmg', ...PUBLISH_FIELDS }, 'skipped publishing');
47
+ log.warn({ file: 'App-0.0.1-mac.zip', ...PUBLISH_FIELDS }, 'skipped publishing');
48
+ log.warn('cannot compute hash of the pending file');
49
+ return ['/tmp/release/App-0.0.1.dmg', '/tmp/release/App-0.0.1-mac.zip'];
50
+ });
51
+
52
+ ctx.expect(result.length).toBe(2);
53
+ ctx.expect(skips.length).toBe(3);
54
+ ctx.expect(skips.map((skip) => skip.file).filter(Boolean)).toEqual(['App-0.0.1.dmg', 'App-0.0.1-mac.zip']);
55
+ ctx.expect(skips[0].reason).toBe(PUBLISH_FIELDS.reason);
56
+ ctx.expect(skips[0].tag).toBe('v0.0.1');
57
+
58
+ // The build's own logging still reaches the logger, and the wrapper is
59
+ // gone the moment the build settles.
60
+ ctx.expect(log.seen.length).toBe(4);
61
+ ctx.expect(log.warn).toBe(original);
62
+ },
63
+ },
64
+ {
65
+ name: 'the wrapper restores the logger even when the build throws',
66
+ run: async (ctx) => {
67
+ const { recordSkippedPublishes } = require(RELEASE_TASK);
68
+ const log = fakeBuilderLog();
69
+ const original = log.warn;
70
+
71
+ let thrown = null;
72
+ try {
73
+ await recordSkippedPublishes(log, async () => { throw new Error('electron-builder exploded'); });
74
+ } catch (error) {
75
+ thrown = error;
76
+ }
77
+
78
+ ctx.expect(thrown.message).toBe('electron-builder exploded');
79
+ ctx.expect(log.warn).toBe(original);
80
+ },
81
+ },
82
+ {
83
+ name: 'a recorded skip fails the release naming the reason, the tag and the fix',
84
+ run: (ctx) => {
85
+ const { skippedUploadError } = require(RELEASE_TASK);
86
+ const skips = [
87
+ { message: 'GitHub release not created', ...PUBLISH_FIELDS },
88
+ { message: 'skipped publishing', file: 'App-0.0.1.dmg', ...PUBLISH_FIELDS },
89
+ ];
90
+
91
+ const error = skippedUploadError(skips);
92
+ ctx.expect(error instanceof Error).toBe(true);
93
+ ctx.expect(error.message).toContain('existing release published more than 2 hours ago');
94
+ ctx.expect(error.message).toContain('v0.0.1');
95
+ ctx.expect(error.message).toContain('App-0.0.1.dmg');
96
+ ctx.expect(error.message).toContain('Bump the version in package.json');
97
+ },
98
+ },
99
+ {
100
+ name: 'a build that skipped nothing is the release it always was',
101
+ run: (ctx) => {
102
+ const { skippedUploadError } = require(RELEASE_TASK);
103
+ ctx.expect(skippedUploadError([])).toBeNull();
104
+ },
105
+ },
106
+ ],
107
+ });