@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
@@ -1,10 +1,11 @@
1
1
  // Build-layer tests for the canonical build verbs (build / package / publish).
2
2
  // The synced projectScripts are thin `omega <verb>` aliases, so each verb must
3
- // run the pipeline ITSELF — clean, certs, gulp task — and never shell back to
3
+ // run the pipeline ITSELF (clean, gulp task) and never shell back to
4
4
  // `npm run build` / `npm run publish` (that would recurse forever).
5
5
  //
6
6
  // There is no `setup` step (#675): the local scaffold runs inside the gulp
7
- // `defaults` task. `certs` delivers the Apple signing artifacts (#678).
7
+ // `defaults` task. Nothing COPIES signing material any more (#891): the tree
8
+ // is read in place and the env load derived the paths to it.
8
9
 
9
10
  const path = require('path');
10
11
  const fs = require('fs');
@@ -26,14 +27,18 @@ function recorder(log) {
26
27
  const record = (type) => (options, step) => { log.push(step.task ? `${type}:${step.task}` : type); };
27
28
  return {
28
29
  clean: record('clean'),
29
- certs: record('certs'),
30
+ 'ship-keys': record('ship-keys'),
30
31
  'validate-certs': record('validate-certs'),
31
32
  gulp: record('gulp'),
32
33
  };
33
34
  }
34
35
 
35
36
  async function withEnv(run) {
36
- const keys = ['OMEGA_BUILD_MODE', 'OMEGA_IS_PUBLISH'];
37
+ // OMEGA_ENVIRONMENT rides the list because it is the ONE environment input
38
+ // ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)) and
39
+ // src/build.js WRITES it at load from the lane, so clearing a lane flag
40
+ // without clearing it leaves the previous lane's word in the process.
41
+ const keys = ['OMEGA_ENVIRONMENT', 'OMEGA_BUILD_MODE', 'OMEGA_IS_PUBLISH'];
37
42
  const previous = keys.map((key) => [key, process.env[key]]);
38
43
  keys.forEach((key) => delete process.env[key]);
39
44
  try {
@@ -51,21 +56,21 @@ module.exports = defineCases({
51
56
  description: 'build verbs — the CLI owns the pipeline and its env flags',
52
57
  tests: [
53
58
  {
54
- name: 'build: OMEGA_BUILD_MODE + clean → certs → gulp build',
59
+ name: 'build: OMEGA_BUILD_MODE + clean → gulp build',
55
60
  run: (ctx) => {
56
61
  const plan = build.plan({});
57
62
  ctx.expect(plan.env.OMEGA_BUILD_MODE).toBe('true');
58
63
  ctx.expect(plan.steps.map((s) => (s.task ? `${s.type}:${s.task}` : s.type)))
59
- .toEqual(['clean', 'certs', 'gulp:build']);
64
+ .toEqual(['clean', 'gulp:build']);
60
65
  },
61
66
  },
62
67
  {
63
- name: 'package: OMEGA_BUILD_MODE + clean → certs → gulp packageBuild',
68
+ name: 'package: OMEGA_BUILD_MODE + clean → gulp packageBuild',
64
69
  run: (ctx) => {
65
70
  const plan = pkg.plan({});
66
71
  ctx.expect(plan.env.OMEGA_BUILD_MODE).toBe('true');
67
72
  ctx.expect(plan.steps.map((s) => (s.task ? `${s.type}:${s.task}` : s.type)))
68
- .toEqual(['clean', 'certs', 'gulp:packageBuild']);
73
+ .toEqual(['clean', 'gulp:packageBuild']);
69
74
  },
70
75
  },
71
76
  {
@@ -73,26 +78,54 @@ module.exports = defineCases({
73
78
  run: (ctx) => {
74
79
  const plan = pkg.plan({ quick: true });
75
80
  ctx.expect(plan.steps.map((s) => (s.task ? `${s.type}:${s.task}` : s.type)))
76
- .toEqual(['clean', 'certs', 'gulp:packageQuick']);
77
- ctx.expect(pkg.plan({ q: true }).steps[2].task).toBe('packageQuick');
81
+ .toEqual(['clean', 'gulp:packageQuick']);
82
+ ctx.expect(pkg.plan({ q: true }).steps[1].task).toBe('packageQuick');
78
83
  },
79
84
  },
80
85
  {
81
- name: 'publish: both flags + certs → validate-certs → gulp publish (no clean)',
86
+ name: 'publish: both flags + ship-keys → validate-certs → gulp publish (no clean)',
82
87
  run: (ctx) => {
83
88
  const plan = publish.plan({});
84
89
  ctx.expect(plan.env.OMEGA_BUILD_MODE).toBe('true');
85
90
  ctx.expect(plan.env.OMEGA_IS_PUBLISH).toBe('true');
86
91
  ctx.expect(plan.steps.map((s) => (s.task ? `${s.type}:${s.task}` : s.type)))
87
- .toEqual(['certs', 'validate-certs', 'gulp:publish']);
92
+ .toEqual(['ship-keys', 'validate-certs', 'gulp:publish']);
88
93
  },
89
94
  },
90
95
  {
91
- name: 'publish --local: clean → certs → validate-certs → gulp publish',
96
+ name: 'publish --local: clean → ship-keys → validate-certs → gulp publish',
92
97
  run: (ctx) => {
93
98
  const plan = publish.plan({ local: true });
94
99
  ctx.expect(plan.steps.map((s) => (s.task ? `${s.type}:${s.task}` : s.type)))
95
- .toEqual(['clean', 'certs', 'validate-certs', 'gulp:publish']);
100
+ .toEqual(['clean', 'ship-keys', 'validate-certs', 'gulp:publish']);
101
+ },
102
+ },
103
+ // #867: a shipped format with no credential is a release that dies on a
104
+ // runner, so publish refuses FIRST, before a minute of build time, naming
105
+ // the key, the declaration that requires it, and the walk that collects it.
106
+ // The SAME JS runs in both lanes: CI's `npm run release:local` is this verb.
107
+ {
108
+ name: 'publish refuses a declared format with no ship credential, naming the key and the walk (#867)',
109
+ run: (ctx) => {
110
+ const { assertShipKeys } = require(path.join(__dirname, '..', '..', '..', 'utils', 'ship-keys.js'));
111
+ const config = { platforms: { linux: { formats: { snap: { channels: ['stable'] } } } } };
112
+
113
+ let message = '';
114
+ try {
115
+ assertShipKeys({ config, env: {} });
116
+ } catch (error) {
117
+ message = error.message;
118
+ }
119
+
120
+ ctx.expect(message).toContain('SNAPCRAFT_STORE_CREDENTIALS (required by platforms.linux.formats.snap)');
121
+ ctx.expect(message).toContain('omega manage --service publishing');
122
+
123
+ // With the credential, the same declaration ships
124
+ ctx.expect(assertShipKeys({ config, env: { SNAPCRAFT_STORE_CREDENTIALS: 'blob' } }).length > 0).toBe(true);
125
+
126
+ // And a brand that never mentions the snap owes nothing: the default
127
+ // format is on, but nothing in its config makes the credential due
128
+ ctx.expect(assertShipKeys({ config: {}, env: {} }).length > 0).toBe(true);
96
129
  },
97
130
  },
98
131
  {
@@ -104,7 +137,7 @@ module.exports = defineCases({
104
137
  ctx.expect(process.env.OMEGA_BUILD_MODE).toBe('true');
105
138
  ctx.expect(process.env.OMEGA_IS_PUBLISH).toBe('true');
106
139
  });
107
- ctx.expect(log).toEqual(['clean', 'certs', 'validate-certs', 'gulp:publish']);
140
+ ctx.expect(log).toEqual(['clean', 'ship-keys', 'validate-certs', 'gulp:publish']);
108
141
  },
109
142
  },
110
143
  {
@@ -17,6 +17,7 @@ const jetpack = require('fs-jetpack');
17
17
  const yaml = require('js-yaml');
18
18
 
19
19
  const { copyDefaults } = require('../../../commands/lib/ensure-target.js');
20
+ const { composeWorkflow } = require('../../../vendor/devkit/ci-workflows.js');
20
21
  const defineCases = require('../../../vendor/devkit/test/define-cases.js');
21
22
 
22
23
  // GitHub takes `needs` as a string or as a list; the graph question is the same.
@@ -51,5 +52,194 @@ module.exports = defineCases({
51
52
  ctx.expect(needsOf(jobs['windows-strategy'])).toContain('build');
52
53
  },
53
54
  },
55
+ // The Windows legs of this workflow force `shell: cmd`
56
+ // ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)), and cmd cannot
57
+ // execute the extension-less `sfw` Socket's action caches on Windows: every
58
+ // windows job died on `'sfw' is not recognized`. devkit renders the shim that
59
+ // copies the binary to `sfw.exe` beside the action step, so the check is on the
60
+ // WRITTEN file, standalone and composed alike.
61
+ {
62
+ name: 'every job that runs sfw carries the Windows cmd shim, standalone and composed (#872)',
63
+ run: async (ctx) => {
64
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-workflow-firewall-'));
65
+ await copyDefaults(tmp);
66
+ const scaffolded = jetpack.read(path.join(tmp, '.github', 'workflows', 'build.yml'));
67
+
68
+ for (const workflow of [scaffolded, composeWorkflow(scaffolded, { targetPath: 'targets/desktop', targetName: 'desktop' })]) {
69
+ // A step the shim broke would not parse at all, so the parse IS an assertion
70
+ const { jobs } = yaml.load(workflow);
71
+
72
+ for (const job of Object.values(jobs)) {
73
+ const steps = job.steps || [];
74
+ const install = steps.findIndex((step) => /\bsfw npm(?:\.cmd)? (ci|install)\b/.test(String(step.run || '')));
75
+ if (install === -1) {
76
+ continue;
77
+ }
78
+
79
+ const firewall = steps.findIndex((step) => String(step.uses || '').startsWith('SocketDev/action@'));
80
+ const shim = steps.findIndex((step) => String(step.run || '').includes('firewall-path-binary'));
81
+
82
+ ctx.expect(firewall).not.toBe(-1);
83
+ ctx.expect(shim).not.toBe(-1);
84
+ ctx.expect(firewall < shim && shim < install).toBe(true);
85
+ ctx.expect(steps[shim].shell).toBe('cmd');
86
+ ctx.expect(steps[shim].if).toContain('Windows');
87
+ }
88
+ }
89
+
90
+ fs.rmSync(tmp, { recursive: true, force: true });
91
+ },
92
+ },
93
+ // The workflow env carries the mac credentials as the base64 secrets themselves,
94
+ // and only the mac job decodes them to disk
95
+ // ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)). A leg that never
96
+ // signs for mac must not hold them: electron-builder would sign with the cert,
97
+ // and the precheck would be handed a .p8 no filesystem can answer for.
98
+ {
99
+ name: 'the mac credentials are blanked on the linux and windows legs, and gated on mac (#872)',
100
+ run: async (ctx) => {
101
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-workflow-creds-'));
102
+ await copyDefaults(tmp);
103
+ const { jobs } = yaml.load(jetpack.read(path.join(tmp, '.github', 'workflows', 'build.yml')));
104
+ const steps = jobs.build.steps;
105
+
106
+ const stepOn = (os_, command) => steps.find((step) => String(step.if || '').includes(os_) && String(step.run || '').includes(command));
107
+
108
+ const linux = stepOn('ubuntu-latest', 'npm run release:local');
109
+ ctx.expect(linux.env.CSC_LINK).toBe('');
110
+ ctx.expect(linux.env.APPLE_API_KEY).toBe('');
111
+
112
+ const windows = stepOn('windows-latest', 'npm run package');
113
+ ctx.expect(windows.env.CSC_LINK).toBe('');
114
+ ctx.expect(windows.env.APPLE_API_KEY).toBe('');
115
+
116
+ // The mac leg is the one that decodes them, and it points at each file only
117
+ // when that secret EXISTS (#872): decoding an empty secret writes an empty
118
+ // file and `security import` of one dies with "parameters not valid".
119
+ // A MISSING one exits 1 (#891): a mac build this framework ships is signed
120
+ // and notarized, and the message names the command that fixes it.
121
+ const decode = stepOn('macos-latest', 'base64 --decode');
122
+ ctx.expect(decode.run).toContain('if [ -n "${{ secrets.CSC_LINK }}" ]; then');
123
+ ctx.expect(decode.run).toContain('if [ -n "${{ secrets.APPLE_API_KEY }}" ]; then');
124
+ // The standalone verb is gone (#891): the deploy precheck pushes them.
125
+ ctx.expect(decode.run).toContain('No CSC_LINK secret: run omega deploy from targets/desktop');
126
+ ctx.expect(decode.run).toContain('No APPLE_API_KEY secret: run omega deploy from targets/desktop');
127
+ ctx.expect(decode.run).not.toContain('push-secrets');
128
+ ctx.expect(decode.run.match(/exit 1/g).length).toBe(2);
129
+ ctx.expect(decode.run).not.toContain('this build will not be signed');
130
+
131
+ const mac = stepOn('macos-latest', 'npm run release:local');
132
+ ctx.expect(mac.env.CSC_LINK).toBe("${{ secrets.CSC_LINK != '' && 'config/certs/dev-id.p12' || '' }}");
133
+ ctx.expect(mac.env.APPLE_API_KEY).toBe("${{ secrets.APPLE_API_KEY != '' && 'config/certs/AuthKey.p8' || '' }}");
134
+
135
+ fs.rmSync(tmp, { recursive: true, force: true });
136
+ },
137
+ },
138
+ // The firewall spawns the command it is given LITERALLY on Windows, with no
139
+ // PATHEXT resolution, and the only `npm` on a Windows runner's PATH is
140
+ // `npm.cmd` (#872): under cmd, bare `npm` is "Command 'npm' not found in PATH".
141
+ {
142
+ name: 'every cmd-shelled install names npm.cmd, every other one stays bare npm (#872)',
143
+ run: async (ctx) => {
144
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-workflow-npmcmd-'));
145
+ await copyDefaults(tmp);
146
+ const { jobs } = yaml.load(jetpack.read(path.join(tmp, '.github', 'workflows', 'build.yml')));
147
+
148
+ for (const job of Object.values(jobs)) {
149
+ const jobShell = job.defaults?.run?.shell;
150
+
151
+ for (const step of job.steps || []) {
152
+ const run = String(step.run || '');
153
+ if (!run.includes('sfw npm')) {
154
+ continue;
155
+ }
156
+
157
+ const underCmd = (step.shell || jobShell) === 'cmd';
158
+ ctx.expect(run.includes('sfw npm.cmd ')).toBe(underCmd);
159
+ }
160
+ }
161
+
162
+ // And the two legs that DO run under cmd are still there to be checked.
163
+ ctx.expect(jobs.build.steps.some((step) => step.shell === 'cmd' && String(step.run || '').includes('sfw npm.cmd ci'))).toBe(true);
164
+ ctx.expect(jobs['windows-sign'].defaults.run.shell).toBe('cmd');
165
+ // The self-hosted signer installs WITHOUT the firewall (#872): its network does
166
+ // not reach Socket's API and the firewall fails closed, while the lockfile it
167
+ // installs was screened by the hosted build jobs of the same run.
168
+ ctx.expect(jobs['windows-sign'].steps.some((step) => String(step.run || '').trim() === 'npm.cmd ci')).toBe(true);
169
+ ctx.expect(jobs['windows-sign'].steps.some((step) => String(step.run || '').includes('sfw '))).toBe(false);
170
+ ctx.expect(jobs['windows-sign'].steps.some((step) => String(step.uses || '').startsWith('SocketDev/action@'))).toBe(false);
171
+
172
+ fs.rmSync(tmp, { recursive: true, force: true });
173
+ },
174
+ },
175
+ // The trigger half of the signer guard
176
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): the box that
177
+ // holds the EV token is reachable by a DISPATCH and nothing else, so a
178
+ // `push` or `pull_request` trigger added to this file later has to SKIP the
179
+ // sign job rather than sign a stray build with it.
180
+ {
181
+ name: 'the sign job runs on a dispatch only, and a skipped sign job still reaches finalize (#875)',
182
+ run: async (ctx) => {
183
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-workflow-signer-guard-'));
184
+ await copyDefaults(tmp);
185
+ const { jobs } = yaml.load(jetpack.read(path.join(tmp, '.github', 'workflows', 'build.yml')));
186
+
187
+ const gate = jobs['windows-sign'].if;
188
+ // The ONE trusted event (#923), ANDed onto the rest so the strategy and
189
+ // platform gates still hold on a dispatch.
190
+ ctx.expect(gate).toContain("&& github.event_name == 'workflow_dispatch'");
191
+ ctx.expect(gate).not.toContain('repository_dispatch');
192
+ ctx.expect(gate).toContain("needs.windows-strategy.outputs.strategy != 'local'");
193
+
194
+ // A SKIP, never a failure: finalize reaches its `if` through `always()`
195
+ // and gates on the BUILD's result, so the unsigned Windows artifact the
196
+ // build job uploaded still finishes the run.
197
+ ctx.expect(jobs.finalize.if).toContain('always()');
198
+ ctx.expect(jobs.finalize.if).toContain("needs.build.result == 'success'");
199
+ ctx.expect(jobs.finalize.if).not.toContain('needs.windows-sign.result');
200
+
201
+ fs.rmSync(tmp, { recursive: true, force: true });
202
+ },
203
+ },
204
+ // The template matches its three siblings in shape
205
+ // ([#880](https://github.com/Omega-JS-Stack/omega/issues/880)): the same
206
+ // header, the same one dispatch trigger, a version log, and a bound on
207
+ // every job. Read off the scaffolded render, like every case above.
208
+ {
209
+ name: 'the scaffolded workflow carries the sibling header, the one dispatch trigger, a version log and a timeout per job (#880)',
210
+ run: async (ctx) => {
211
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-workflow-shape-'));
212
+ await copyDefaults(tmp);
213
+ const scaffolded = jetpack.read(path.join(tmp, '.github', 'workflows', 'build.yml'));
214
+ const doc = yaml.load(scaffolded);
215
+
216
+ // The header opens the file, above `name:`, the way backend's does.
217
+ ctx.expect(scaffolded.startsWith('# Deliberate deploys (D13):')).toBe(true);
218
+ ctx.expect(scaffolded).toContain("Regenerated by every verb's ensureTarget");
219
+
220
+ // The one dispatch trigger (#923), carrying the `platforms` input.
221
+ // js-yaml 4 keeps `on` a plain string key (YAML 1.2 drops the
222
+ // `on`/`off` booleans YAML 1.1 had), so `doc.on` is the trigger map.
223
+ ctx.expect(Object.keys(doc.on)).toEqual(['workflow_dispatch']);
224
+ ctx.expect(doc.on.workflow_dispatch.inputs).toHaveProperty('platforms');
225
+
226
+ // ONE version log for all three legs of the build matrix, under an
227
+ // explicit `shell: bash` (the cmd forcing is for npm, this step reads).
228
+ const log = doc.jobs.build.steps.find((step) => step.name === 'Log versions');
229
+ ctx.expect(log).toBeDefined();
230
+ ctx.expect(log.shell).toBe('bash');
231
+ for (const command of ['node -v', 'npm -v', 'node "${{ github.workspace }}/node_modules/@omega.js/desktop/bin/omega" version']) {
232
+ ctx.expect(log.run).toContain(command);
233
+ }
234
+
235
+ // No job holds a runner for GitHub's six-hour default.
236
+ const unbounded = Object.entries(doc.jobs)
237
+ .filter(([, job]) => typeof job['timeout-minutes'] !== 'number')
238
+ .map(([name]) => name);
239
+ ctx.expect(unbounded).toEqual([]);
240
+
241
+ fs.rmSync(tmp, { recursive: true, force: true });
242
+ },
243
+ },
54
244
  ],
55
245
  });
@@ -51,30 +51,52 @@ module.exports = defineCases({
51
51
  },
52
52
  },
53
53
  {
54
- name: 'bin/omega-desktop exists',
54
+ name: 'bin/omega exists',
55
55
  run: (ctx) => {
56
- const binFile = path.join(root, 'bin', 'omega-desktop');
56
+ const binFile = path.join(root, 'bin', 'omega');
57
57
  ctx.expect(fs.existsSync(binFile)).toBeTruthy();
58
58
  },
59
59
  },
60
60
  {
61
- name: 'bin/omega-desktop is executable',
61
+ name: 'bin/omega is executable',
62
62
  run: (ctx) => {
63
63
  // NTFS has no execute bit, so the mode carries nothing to assert on a
64
64
  // Windows checkout — the existence case above still runs everywhere.
65
65
  if (process.platform === 'win32') ctx.skip('NTFS has no execute bit — `stat.mode & 0o100` is meaningless on Windows');
66
- const stat = fs.statSync(path.join(root, 'bin', 'omega-desktop'));
66
+ const stat = fs.statSync(path.join(root, 'bin', 'omega'));
67
67
  ctx.expect((stat.mode & 0o100) !== 0).toBeTruthy();
68
68
  },
69
69
  },
70
70
  {
71
- name: 'cli-run disables yargs built-in --help/--version (router owns both)',
71
+ name: 'cli-run declares its value-less flags and claims neither --help nor --version (the router owns both)',
72
72
  run: (ctx) => {
73
- // The built-ins printed an empty stub and version "0.0.0" instead of
74
- // reaching the alias table / the router's generated help (wave-6 D1).
75
- const source = fs.readFileSync(path.join(root, 'dist', 'cli-run.js'), 'utf8');
76
- ctx.expect(source.includes('.version(false)')).toBeTruthy();
77
- ctx.expect(source.includes('.help(false)')).toBeTruthy();
73
+ // The parse the bin really performs (#920). The built-ins it replaced
74
+ // printed an empty stub and version "0.0.0" instead of reaching the
75
+ // alias table / the router's generated help (wave-6 D1).
76
+ const { BOOLEAN_FLAGS } = require(path.join(root, 'dist', 'cli-run.js'));
77
+ const { parseArgv } = require('../../../vendor/devkit/argv.js');
78
+ const parse = (args) => parseArgv(args, { booleans: BOOLEAN_FLAGS });
79
+
80
+ // A value-less flag never swallows the next positional.
81
+ const scoped = parse(['test', '--extended', 'mgr:build/cli']);
82
+ ctx.expect(scoped.extended).toBe(true);
83
+ ctx.expect(scoped._.join(' ')).toBe('test mgr:build/cli');
84
+
85
+ // A value flag keeps its value in the SPACE-separated form CI writes
86
+ // (src/defaults/.github/workflows/build.yml).
87
+ const signing = parse(['sign-windows', '--in', 'release', '--out', 'release/signed']);
88
+ ctx.expect(signing.in).toBe('release');
89
+ ctx.expect(signing.out).toBe('release/signed');
90
+ ctx.expect(parse(['finalize-release', '--signed-dir', 'release/signed']).signedDir).toBe('release/signed');
91
+
92
+ // Every value-LESS flag is declared, so none of them can eat the token
93
+ // after it: `omega logs --tail runtime` keeps the log name.
94
+ const tailed = parse(['logs', '--tail', 'runtime']);
95
+ ctx.expect(tailed.tail).toBe(true);
96
+ ctx.expect(tailed._.join(' ')).toBe('logs runtime');
97
+
98
+ // And --help reaches the router rather than a built-in.
99
+ ctx.expect(parse(['deploy', '--help']).help).toBe(true);
78
100
  },
79
101
  },
80
102
  {
@@ -113,12 +113,12 @@ module.exports = defineCases({
113
113
  },
114
114
  },
115
115
  {
116
- name: 'resolved: has platforms.win.signing block (Windows-specific signing config)',
116
+ name: 'resolved: has platforms.windows.signing block (Windows-specific signing config)',
117
117
  run: (ctx) => {
118
118
  ctx.expect(ctx.state.cfg.platforms).toBeTruthy();
119
- ctx.expect(ctx.state.cfg.platforms.win).toBeTruthy();
120
- ctx.expect(ctx.state.cfg.platforms.win.signing).toBeTruthy();
121
- ctx.expect(['self-hosted', 'cloud', 'local']).toContain(ctx.state.cfg.platforms.win.signing.strategy);
119
+ ctx.expect(ctx.state.cfg.platforms.windows).toBeTruthy();
120
+ ctx.expect(ctx.state.cfg.platforms.windows.signing).toBeTruthy();
121
+ ctx.expect(['self-hosted', 'cloud', 'local']).toContain(ctx.state.cfg.platforms.windows.signing.strategy);
122
122
  },
123
123
  },
124
124
  {
@@ -0,0 +1,51 @@
1
+ // Build-layer tests for the locked-console refusal. The process list is
2
+ // injected, so both verdicts are provable without a Windows box.
3
+
4
+ const path = require('path');
5
+ const defineCases = require('../../../vendor/devkit/test/define-cases.js');
6
+
7
+ const lock = require(path.join(__dirname, '..', '..', '..', 'lib', 'sign-helpers', 'console-lock.js'));
8
+
9
+ module.exports = defineCases({
10
+ type: 'group',
11
+ layer: 'build',
12
+ description: 'console-lock — LogonUI.exe in the process list is a locked console',
13
+ tests: [
14
+ {
15
+ name: "tasklist's CSV with the lock-screen process reads as locked, without it as not",
16
+ run: (ctx) => {
17
+ ctx.expect(lock.isConsoleLocked('"LogonUI.exe","2012","Console","1","71,284 K"\r\n')).toBe(true);
18
+ ctx.expect(lock.isConsoleLocked('INFO: No tasks are running which match the specified criteria.')).toBe(false);
19
+ ctx.expect(lock.isConsoleLocked('')).toBe(false);
20
+ },
21
+ },
22
+ {
23
+ name: 'a locked console is refused, naming LogonUI.exe and the fix',
24
+ run: async (ctx) => {
25
+ const ran = [];
26
+ const exec = (out) => async (cmd) => { ran.push(cmd); return out; };
27
+
28
+ let threw;
29
+ try {
30
+ await lock.assertConsoleUnlocked({ platform: 'win32', exec: exec('"LogonUI.exe","2012","Console","1","71,284 K"') });
31
+ } catch (e) { threw = e; }
32
+ ctx.expect(threw).toBeDefined();
33
+ ctx.expect(threw.message).toMatch(/console session is locked/);
34
+ ctx.expect(threw.message).toMatch(/LogonUI\.exe/);
35
+ ctx.expect(threw.message).toMatch(/Unlock the box/);
36
+ // The filter and the headerless CSV are what isConsoleLocked reads.
37
+ ctx.expect(ran).toEqual(['tasklist /fi "IMAGENAME eq LogonUI.exe" /fo csv /nh']);
38
+
39
+ await lock.assertConsoleUnlocked({ platform: 'win32', exec: exec('INFO: No tasks are running which match the specified criteria.') });
40
+ },
41
+ },
42
+ {
43
+ name: 'off Windows there is no lock screen and nothing is run',
44
+ run: async (ctx) => {
45
+ const ran = [];
46
+ await lock.assertConsoleUnlocked({ platform: 'darwin', exec: async (cmd) => { ran.push(cmd); return '"LogonUI.exe"'; } });
47
+ ctx.expect(ran).toEqual([]);
48
+ },
49
+ },
50
+ ],
51
+ });
@@ -107,8 +107,77 @@ module.exports = defineCases({
107
107
  const gitignore = jetpack.read(path.join(tmp, '.gitignore'));
108
108
  const customPart = gitignore.slice(gitignore.indexOf(CUSTOM_MARKER));
109
109
  ctx.expect(customPart).toContain('my-secret-dir/');
110
- // Not in @omega.js/desktop's defaults → migrated below the Custom marker.
111
- ctx.expect(customPart).toContain('user-added-dir/');
110
+ // The framework block is completely managed ([#926](https://github.com/Omega-JS-Stack/omega/issues/926)):
111
+ // a line inside it that @omega.js/desktop's defaults no longer carry leaves the
112
+ // file. Lines the consumer owns go below the Custom marker.
113
+ ctx.expect(gitignore).not.toContain('user-added-dir/');
114
+ },
115
+ },
116
+ {
117
+ name: 'config/certs/: the target .gitignore hides the material and tracks the README (#913)',
118
+ run: async (ctx) => {
119
+ const { spawnSync } = require('child_process');
120
+
121
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-defaults-'));
122
+ await copyDefaults(tmp);
123
+
124
+ const gitignore = jetpack.read(path.join(tmp, '.gitignore'));
125
+ const defaultPart = gitignore.slice(0, gitignore.indexOf(CUSTOM_MARKER));
126
+ ctx.expect(defaultPart).toContain('config/certs/*');
127
+ ctx.expect(defaultPart).toContain('!config/certs/README.md');
128
+ // The framework section is a file consumers edit, and the dotfiles emdash
129
+ // hook bounces one (built by code point so this file carries none either).
130
+ ctx.expect(defaultPart.includes(String.fromCharCode(0x2014))).toBe(false);
131
+
132
+ // The README the scaffold writes says the target .gitignore is what
133
+ // hides this directory, so no sibling .gitignore ships beside it.
134
+ ctx.expect(jetpack.exists(path.join(tmp, 'config', 'certs', 'README.md'))).toBe('file');
135
+ ctx.expect(jetpack.exists(path.join(tmp, 'config', 'certs', '.gitignore'))).toBe(false);
136
+
137
+ // git is the only authority on this: prove it against a real repo.
138
+ jetpack.write(path.join(tmp, 'config', 'certs', 'foo.p12'), 'not a real cert');
139
+ spawnSync('git', ['init', '-q'], { cwd: tmp });
140
+ const ignored = (file) => spawnSync('git', ['check-ignore', '-q', file], { cwd: tmp }).status === 0;
141
+
142
+ ctx.expect(ignored('config/certs/README.md')).toBe(false);
143
+ ctx.expect(ignored('config/certs/foo.p12')).toBe(true);
144
+ },
145
+ },
146
+ {
147
+ name: 'config/certs/: a target scaffolded before #913 converges on the next verb run',
148
+ run: async (ctx) => {
149
+ // Three leftovers each keep the README hidden on their own, so the next
150
+ // verb run heals all three: the old `config/certs/` line, which the
151
+ // marker merge now DROPS with the rest of the retired framework block
152
+ // ([#926](https://github.com/Omega-JS-Stack/omega/issues/926)), the
153
+ // sibling `.gitignore` (a nested ignore file beats the parent), and the
154
+ // README's own sentence, which cited that sibling.
155
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-defaults-'));
156
+ const sibling = path.join(tmp, 'config', 'certs', '.gitignore');
157
+ const readme = path.join(tmp, 'config', 'certs', 'README.md');
158
+
159
+ jetpack.write(
160
+ path.join(tmp, '.gitignore'),
161
+ `${DEFAULT_MARKER}\n.env\nconfig/certs/\n*.p12\n\n${CUSTOM_MARKER}\nmy-secret-dir/\n`
162
+ );
163
+ jetpack.write(sibling, '*\n!.gitignore\n');
164
+ jetpack.write(readme, '# `config/certs/`\n\nThe `.gitignore` beside this file ignores everything it holds.\n');
165
+
166
+ await copyDefaults(tmp);
167
+
168
+ const gitignore = jetpack.read(path.join(tmp, '.gitignore'));
169
+ ctx.expect(gitignore.split('\n').some((line) => line.trim() === 'config/certs/')).toBe(false);
170
+ const defaultHalf = gitignore.slice(0, gitignore.indexOf(CUSTOM_MARKER));
171
+ ctx.expect(defaultHalf).toContain('config/certs/*');
172
+ ctx.expect(defaultHalf).toContain('!config/certs/README.md');
173
+ ctx.expect(gitignore).toContain('my-secret-dir/');
174
+ ctx.expect(jetpack.exists(sibling)).toBe(false);
175
+ ctx.expect(jetpack.read(readme)).toContain("The target's `.gitignore` ignores everything here except this file");
176
+
177
+ // Anything else in that sibling is the consumer's, and is never destroyed.
178
+ jetpack.write(sibling, '*\n!.gitignore\n!my-own-note.txt\n');
179
+ await copyDefaults(tmp);
180
+ ctx.expect(jetpack.exists(sibling)).toBe('file');
112
181
  },
113
182
  },
114
183
  {