@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,50 +1,64 @@
1
- // release — trigger the GitHub Actions Build & Release workflow + stream logs locally.
1
+ // release: trigger the GitHub Actions Build & Release workflow, then follow the
2
+ // run it started.
2
3
  //
3
4
  // Replaces the old "do it from my laptop" release flow with "let CI do it, but make it
4
5
  // feel local." User runs `npm run release` (or `npx omega release`) and gets:
5
- // 1. A workflow_dispatch POST to GH Actions on the brand's repo (owner/repo from the
6
- // config, and the workflow file the scaffold actually wrote: see dispatchTarget).
7
- // 2. A few seconds of waiting while GH spins up the run.
8
- // 3. Live polling of every job's logs at 5s intervals, printing NEW lines as they
9
- // arrive (job-prefixed) so it looks like streaming.
10
- // 4. Everything teed to <root>/logs/ci.log with ANSI codes preserved on stdout
11
- // and stripped from the file (matches the dev/build log convention).
12
- // 5. Exit 0 on success, 1 on any job failure.
13
- //
14
- // Why poll instead of stream? GH Actions doesn't expose live stdout — logs are only
15
- // fetchable AFTER each STEP completes. So "streaming" is a polite fiction: every 5s
16
- // we re-fetch each job's logs and diff against what we already printed.
6
+ // 1. The ONE deploy lane every target takes (`@omega.js/devkit/deploy`'s
7
+ // deployViaDispatch, #872, #915): the composed workflow files reach the
8
+ // default branch when they differ there (GitHub registers a workflow from
9
+ // that branch), the brand folder is force-pushed to `omega-deploy` (packed
10
+ // when it is linked), and the workflow is dispatched on that ref (the
11
+ // brand's own repo and the composed workflow name: devkit's
12
+ // dispatchTarget, #847).
13
+ // 2. The ONE run follower every dispatched deploy uses
14
+ // (`@omega.js/devkit/deploy-follow`, [#873](https://github.com/Omega-JS-Stack/omega/issues/873)):
15
+ // it finds the run, polls it, prints each job's log with the job's name in
16
+ // front, and exits 1 on any conclusion but success. This file owned that
17
+ // follower alone until #873 gave the other three targets the same one.
18
+ // 3. Everything teed to <root>/logs/deploy.log, the one name every target's
19
+ // deploy writes (docs/shared/logging.md), ANSI stripped from the file.
17
20
 
18
21
  const path = require('path');
19
- const fs = require('fs');
20
- const jetpack = require('fs-jetpack');
21
22
 
22
- const { getOctokit } = require('../utils/github.js');
23
- const { dispatchRepo } = require('../vendor/devkit/deploy.js');
23
+ const attachLogFile = require('../utils/attach-log-file.js');
24
+ const { deployViaDispatch, dispatchTarget, laneLabel, resolveToken } = require('../vendor/devkit/deploy.js');
25
+ const { followRun } = require('../vendor/devkit/deploy-follow.js');
26
+ const { targetNameFromDir } = require('../vendor/config/index.js');
24
27
  const Manager = new (require('../build.js'));
25
28
 
26
29
  const logger = Manager.logger('release');
27
30
 
28
- const POLL_INTERVAL_MS = 5000;
29
-
30
31
  module.exports = async function release(options = {}) {
31
32
  const projectRoot = process.cwd();
32
-
33
- if (!process.env.GH_TOKEN) {
34
- throw new Error('GH_TOKEN not set in env. Set it in .env (or shell) so we can dispatch the workflow.');
35
- }
36
-
37
- // silent: true — suppress octokit's default console output for transient 404s
38
- // (in-progress job logs return 404 until the step completes).
39
- const octokit = getOctokit({ silent: true });
40
- if (!octokit) {
41
- throw new Error('Failed to create octokit (missing GH_TOKEN?).');
33
+ // The DRY RUN arrives here too ([#895](https://github.com/Omega-JS-Stack/omega/issues/895)):
34
+ // this verb is desktop's one dispatch call site, so the plan a preview prints
35
+ // is built by the very call a real run sends, threading the ref and the
36
+ // snapshot sha the same way.
37
+ const dryRun = !!(options.dryRun || options['dry-run']);
38
+
39
+ // The whole verb in one file, from its first line (#873), the token refusal
40
+ // below included: a caller that hands the writers back afterwards then pops
41
+ // THIS layer, never the one under it. A no-op when `omega deploy` delegated
42
+ // here and already attached this exact path.
43
+ const logPath = path.join(projectRoot, 'logs', 'deploy.log');
44
+ attachLogFile(logPath);
45
+
46
+ // The ONE token chain devkit resolves for every target, `GH_TOKEN` →
47
+ // `GITHUB_TOKEN` → `gh auth token`, and the very one the follower below is
48
+ // handed: testing the bare variable refused a machine signed in with `gh`
49
+ // and holding no variable at all, which is most of them. A dry run needs
50
+ // none: every step under it is a read or a plan.
51
+ if (!dryRun && !resolveToken()) {
52
+ throw new Error('No GitHub token: set GH_TOKEN in .env (or GITHUB_TOKEN in the shell), or sign in with `gh auth login`, so we can dispatch the workflow.');
42
53
  }
43
54
 
44
- const { owner, repo, workflow: WORKFLOW_FILE } = dispatchTarget({ projectRoot, config: Manager.getConfig() });
45
-
46
- // Discover ref (current branch or override via --ref).
47
- const ref = options.ref || (await currentBranch(projectRoot)) || 'main';
55
+ // The CI dispatch address, from the config and the scaffold: the brand's own
56
+ // repo and the workflow file the target's ensure-target pass actually wrote
57
+ // (composed as `desktop-build.yml` at the brand root inside a monorepo, plain
58
+ // `build.yml` standalone). ONE helper for all four targets
59
+ // ([#847](https://github.com/Omega-JS-Stack/omega/issues/847)), where this
60
+ // file used to keep desktop's own copy of it.
61
+ const { owner, repo, workflow: WORKFLOW_FILE } = dispatchTarget({ projectRoot, config: Manager.getConfig(), workflow: 'build.yml' });
48
62
 
49
63
  // Optional --platforms / --platform flag forwarded as a workflow input. Accepts a
50
64
  // single value ('windows') or comma-separated list ('mac,linux'). Special value
@@ -53,234 +67,67 @@ module.exports = async function release(options = {}) {
53
67
  // workflows (without a `platforms` input declared) keep working unchanged.
54
68
  const platforms = options.platforms || options.platform || null;
55
69
 
56
- const dispatchArgs = { owner, repo, workflow_id: WORKFLOW_FILE, ref };
57
- if (platforms) dispatchArgs.inputs = { platforms: String(platforms) };
58
-
59
70
  const platformsLabel = platforms ? ` (platforms=${platforms})` : '';
60
- logger.log(`Triggering ${owner}/${repo} workflow ${WORKFLOW_FILE} on ref=${ref}${platformsLabel}...`);
61
-
62
- // 1. Mark a "before" timestamp so we can identify the new run we just dispatched.
63
- const before = new Date();
64
-
65
- // 2. Dispatch.
66
- await octokit.rest.actions.createWorkflowDispatch(dispatchArgs);
67
-
68
- // 3. Wait for the new run to appear (GH Actions takes a few seconds to register it).
69
- const run = await waitForNewRun({ octokit, owner, repo, after: before, workflowFile: WORKFLOW_FILE });
70
- if (!run) {
71
- throw new Error('Workflow dispatch succeeded but no new run appeared after 60s. Check GitHub Actions UI.');
72
- }
73
-
74
- logger.log(`Run started: ${run.html_url}`);
75
- require('../vendor/devkit/deploy-record.js').recordDeploy({ dir: projectRoot, target: 'desktop', detail: { method: 'release-dispatch' } });
76
-
77
- // 4. Set up the CI log tee.
78
- const logsDir = path.join(projectRoot, 'logs');
79
- jetpack.dir(logsDir);
80
- const ciLogPath = path.join(logsDir, 'ci.log');
81
- // Truncate so each release run starts fresh.
82
- fs.writeFileSync(ciLogPath, `# release run ${run.html_url}\n# started ${new Date().toISOString()}\n\n`);
83
- const logFile = fs.createWriteStream(ciLogPath, { flags: 'a' });
84
-
85
- // Spinner state. We tick every 250ms even between polls so the terminal feels alive.
86
- // The spinner line uses \r to overwrite itself; before printing real content we clear
87
- // the line, write content, then re-render the spinner. File output is unaffected.
88
- const startedAt = Date.now();
89
- const isTty = process.stdout.isTTY === true;
90
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
91
- let spinnerFrame = 0;
92
- let pollCount = 0;
93
- let latestState = null;
94
- let latestJobs = [];
95
-
96
- function clearSpinner() {
97
- if (!isTty) return;
98
- process.stdout.write('\r\x1b[2K');
99
- }
100
-
101
- function renderSpinner() {
102
- if (!isTty || !latestState) return;
103
- const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
104
- const elapsed = formatElapsed(Date.now() - startedAt);
105
- const status = `${latestState.status}${latestState.conclusion ? ` (${latestState.conclusion})` : ''}`;
106
- const jobBits = latestJobs.map((j) => `${jobSymbol(j)} ${j.name}`).join(' ');
107
- const line = `${frame} ${status} · ${elapsed} · ${pollCount} polls · ${jobBits}`;
108
- // Truncate so we never wrap (which breaks the \r overwrite).
109
- const cols = process.stdout.columns || 120;
110
- const safe = line.length > cols - 1 ? line.slice(0, cols - 2) + '…' : line;
111
- process.stdout.write(`\r${safe}`);
112
- }
113
-
114
- // Print line(s) to stdout AND tee to file. Clears spinner first; spinner re-renders next tick.
115
- function print(line) {
116
- clearSpinner();
117
- process.stdout.write(line);
118
- logFile.write(stripAnsi(line));
71
+ if (!dryRun) {
72
+ logger.log(`Triggering ${owner}/${repo} workflow ${WORKFLOW_FILE}${platformsLabel}...`);
119
73
  }
120
74
 
121
- // Tick the spinner ~4 times/sec so it animates between polls.
122
- const spinnerTimer = isTty ? setInterval(() => {
123
- spinnerFrame += 1;
124
- renderSpinner();
125
- }, 250) : null;
75
+ // 1. Mark a "before" timestamp so the follower identifies the run we just
76
+ // dispatched rather than the one before it.
77
+ const since = new Date();
126
78
 
127
- // 5. Poll until completion. Track byte offsets per job so we only print new lines.
128
- const printedByJob = new Map(); // jobId -> chars printed so far
129
- let lastStatusLine = '';
130
-
131
- try {
132
- while (true) {
133
- pollCount += 1;
134
- const { data: state } = await octokit.rest.actions.getWorkflowRun({
135
- owner, repo, run_id: run.id,
136
- });
137
-
138
- // Pull all jobs. Each job has steps; each step has a step-level status.
139
- const { data: jobsResp } = await octokit.rest.actions.listJobsForWorkflowRun({
140
- owner, repo, run_id: run.id, per_page: 100,
141
- });
142
- const jobs = jobsResp.jobs || [];
143
- latestState = state;
144
- latestJobs = jobs;
145
-
146
- // For each job, fetch its logs (only available once steps complete; for in-progress
147
- // jobs, GH returns 404 or partial — we tolerate both).
148
- for (const job of jobs) {
149
- // Skip queued jobs entirely (no logs yet).
150
- if (job.status === 'queued') continue;
151
-
152
- let logsText = '';
153
- try {
154
- const { data } = await octokit.request('GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs', {
155
- owner, repo, job_id: job.id,
156
- });
157
- // Octokit returns the raw text content here.
158
- logsText = typeof data === 'string' ? data : Buffer.from(data || '').toString('utf8');
159
- } catch (e) {
160
- // 404 = logs not ready yet. Other errors we just skip until next tick.
161
- continue;
162
- }
163
-
164
- const printed = printedByJob.get(job.id) || 0;
165
- if (logsText.length > printed) {
166
- const fresh = logsText.slice(printed);
167
- // Prefix each line with the job name so interleaved output is readable.
168
- const prefix = `[${job.name}] `;
169
- for (const rawLine of fresh.split('\n')) {
170
- if (!rawLine) continue;
171
- print(`${prefix}${rawLine}\n`);
172
- }
173
- printedByJob.set(job.id, logsText.length);
174
- }
175
- }
176
-
177
- // Print a one-line status banner if it changed (no spam — only on transition).
178
- const banner = formatStatusBanner(state, jobs);
179
- if (banner !== lastStatusLine) {
180
- print(`\n${banner}\n`);
181
- lastStatusLine = banner;
182
- }
183
-
184
- if (state.status === 'completed') {
185
- clearSpinner();
186
- logFile.end();
187
- const success = state.conclusion === 'success';
188
- const symbol = success ? '✓' : '✗';
189
- logger.log(`${symbol} Run ${state.conclusion} — ${state.html_url}`);
190
- logger.log(`Logs: ${path.relative(projectRoot, ciLogPath)}`);
191
- if (!success) {
192
- process.exitCode = 1;
193
- throw new Error(`Release run failed (conclusion=${state.conclusion}). See ${ciLogPath} or ${state.html_url}.`);
194
- }
195
- return;
196
- }
197
-
198
- await sleep(POLL_INTERVAL_MS);
199
- }
200
- } finally {
201
- if (spinnerTimer) clearInterval(spinnerTimer);
202
- clearSpinner();
203
- }
204
- };
205
-
206
- // The CI dispatch address, from the config and the scaffold: the brand's APP
207
- // repo (`@omega.js/devkit/deploy`'s dispatchRepo, the one call web and the
208
- // extension deploy verbs make too) and the workflow file the target's
209
- // ensure-target pass actually wrote (composed as `desktop-build.yml` at the
210
- // brand root inside a monorepo, plain `build.yml` standalone:
211
- // `@omega.js/devkit/ci-workflows` owns that name for all four frameworks).
212
- // Never the git remote: a brand nested in another repo would dispatch on the
213
- // enclosing one, which for the playground is the framework monorepo itself.
214
- // Exported for tests.
215
- function dispatchTarget({ projectRoot, config }) {
216
- const { composedWorkflowName } = require('../vendor/devkit/ci-workflows.js');
217
- const { resolveSeedMode } = require('../vendor/config/index.js');
218
- const { owner, repo } = dispatchRepo(config);
219
-
220
- return {
79
+ // 2. Deliver the brand and dispatch, through the ONE lane (#872, #915): the
80
+ // composed workflows to the repo's default branch when they differ, the brand
81
+ // folder to `omega-deploy`, which is the ref this dispatches. An explicit
82
+ // `--ref` still wins.
83
+ const { plan, lane, sha } = await deployViaDispatch({
84
+ workflow: WORKFLOW_FILE,
221
85
  owner,
222
86
  repo,
223
- workflow: composedWorkflowName({
224
- targetDir: projectRoot,
225
- brandRoot: resolveSeedMode(projectRoot).brandRoot,
226
- workflow: 'build.yml',
227
- }),
228
- };
229
- }
230
-
231
- function formatElapsed(ms) {
232
- const totalSec = Math.floor(ms / 1000);
233
- const m = Math.floor(totalSec / 60);
234
- const s = totalSec % 60;
235
- return m > 0 ? `${m}m ${String(s).padStart(2, '0')}s` : `${s}s`;
236
- }
237
-
238
- function jobSymbol(j) {
239
- if (j.status === 'completed') return j.conclusion === 'success' ? '✓' : '✗';
240
- if (j.status === 'in_progress') return '…';
241
- if (j.status === 'queued') return '·';
242
- return '?';
243
- }
244
-
245
- async function waitForNewRun({ octokit, owner, repo, after, workflowFile }) {
246
- const deadline = Date.now() + 60_000;
247
- while (Date.now() < deadline) {
248
- const { data } = await octokit.rest.actions.listWorkflowRuns({
249
- owner, repo, workflow_id: workflowFile, per_page: 5, event: 'workflow_dispatch',
250
- });
251
- const runs = data.workflow_runs || [];
252
- // The newest run created strictly after our dispatch timestamp.
253
- const fresh = runs.find((r) => new Date(r.created_at).getTime() >= after.getTime() - 1000);
254
- if (fresh) return fresh;
255
- await sleep(2000);
87
+ dir: projectRoot,
88
+ ref: options.ref,
89
+ inputs: platforms ? { platforms: String(platforms) } : undefined,
90
+ // The brand root's one snapshot for the whole fan-out, when a brand-root
91
+ // deploy spawned this verb (#901): the push is done, so this run
92
+ // dispatches against that sha instead of pushing over it. Nobody types it.
93
+ snapshot: options.snapshot,
94
+ dryRun,
95
+ logger,
96
+ });
97
+
98
+ // The plan IS the dry run: what would be sent, and nothing sent.
99
+ if (dryRun) {
100
+ logger.log(`DRY RUN (${laneLabel(lane)}), would send:`);
101
+ logger.log(` ${plan.method} ${plan.url}`);
102
+ logger.log(` body: ${JSON.stringify(plan.body)}`);
103
+ logger.log(` then watch: ${plan.runsUrl}`);
104
+ return;
256
105
  }
257
- return null;
258
- }
259
106
 
260
- function formatStatusBanner(run, jobs) {
261
- const parts = jobs.map((j) => `${jobSymbol(j)} ${j.name}`);
262
- return `── ${run.status}${run.conclusion ? ` (${run.conclusion})` : ''} ── ${parts.join(' | ')}`;
263
- }
107
+ logger.log(`Dispatched ${WORKFLOW_FILE} (${laneLabel(lane, sha)}): CI builds, signs, and publishes the release artifacts.`);
108
+ // The ONE runs-url formula is the plan's (`buildDispatch`), the same line
109
+ // backend, web and the extension print: rebuilt by hand here, it was a second
110
+ // copy of an address the dispatch already answered.
111
+ logger.log(`Watch: ${plan.runsUrl}`);
264
112
 
265
- async function currentBranch(projectRoot) {
266
- try {
267
- const { execute } = require('node-powertools');
268
- const out = await execute('git rev-parse --abbrev-ref HEAD', { cwd: projectRoot, log: false });
269
- return String(out || '').trim();
270
- } catch (e) {
271
- return null;
272
- }
273
- }
113
+ require('../vendor/devkit/deploy-record.js').recordDeploy({ dir: projectRoot, target: targetNameFromDir(projectRoot) || 'desktop', detail: { method: 'release-dispatch' } });
274
114
 
275
- function sleep(ms) {
276
- return new Promise((resolve) => setTimeout(resolve, ms));
277
- }
115
+ // 3. Follow the run to its verdict: the jobs' logs stream in here, and a red
116
+ // run throws, so this verb exits 1 on anything but success (#873). The run
117
+ // also has to be building the tree this deploy pushed (#902): on the push
118
+ // lane there is no sha and the check is off, because that lane's run head is
119
+ // the developer's own commit.
120
+ const { conclusion } = await followRun({
121
+ owner,
122
+ repo,
123
+ workflow: WORKFLOW_FILE,
124
+ since,
125
+ headSha: sha,
126
+ token: resolveToken(),
127
+ logger,
128
+ });
278
129
 
279
- function stripAnsi(s) {
280
- // Minimal ANSI stripper — handles CSI sequences (colors, cursor moves).
281
- return String(s).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
282
- }
130
+ logger.log(`Logs: ${path.relative(projectRoot, logPath)}`);
283
131
 
284
- // Exported for tests + `omega deploy`, which prints the exact dispatch this
285
- // sends before delegating here.
286
- module.exports.dispatchTarget = dispatchTarget;
132
+ return conclusion;
133
+ };
@@ -57,6 +57,7 @@ function ghRunnerName(org) {
57
57
  // — is `<home>\.env`, read here before anything looks at process.env, so every
58
58
  // subcommand works from any directory (utils/runner-env.js).
59
59
  const { defaultRunnerHome, runnerEnvFile, runnerLogFile, loadRunnerEnv, ensureRunnerConfig, reconfigureRunnerEnv, runnerEnvReport, parseRunnerOrgs, sameRunnerOrgs, isTestRun, isScratchRunnerHome, assertTestSafeRunnerHome, runnerPrivateHome, ensureRunnerPrivateHome, ensureRunnerDirEnv } = require('../utils/runner-env.js');
60
+ const { ensureRunnerJobGuard, jobGuardListFiles } = require('../utils/runner-job-guard.js');
60
61
  const attachLogFile = require('../utils/attach-log-file.js');
61
62
  const RUNNER_HOME = process.env.OMEGA_RUNNER_HOME || defaultRunnerHome();
62
63
  loadRunnerEnv({ home: RUNNER_HOME });
@@ -480,9 +481,27 @@ async function registerOrg(options) {
480
481
  // runner's `<runner dir>\.env`, which the listener reads at startup and
481
482
  // applies to every job, so the detached spawn and the Startup shortcut both
482
483
  // get it from one write.
484
+ //
485
+ // The job guard travels the same way
486
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): the hook the
487
+ // listener runs before a job's first step, refusing anything that is not a
488
+ // dispatch from a listed repo by a listed actor. The actor list starts as the
489
+ // `GH_TOKEN` user, which is this box owner's account and the account a
490
+ // `workflow_dispatch` from the deploy token carries (Ian 2026-09-13); a
491
+ // lookup that does not answer leaves the file for the operator rather than
492
+ // failing a registration over it.
483
493
  const privateHome = ensureRunnerPrivateHome(home, { logger });
484
- ensureRunnerDirEnv(runnerDir, { HOME: privateHome.home });
494
+ let actor;
495
+ try {
496
+ const { data } = await octokit.rest.users.getAuthenticated();
497
+ actor = data.login;
498
+ } catch (e) {
499
+ logger.warn(` Could not read the GH_TOKEN user for the job guard's actor list: ${e.message}`);
500
+ }
501
+ const guard = ensureRunnerJobGuard(home, { orgs: [org], actor, logger });
502
+ ensureRunnerDirEnv(runnerDir, { HOME: privateHome.home, ACTIONS_RUNNER_HOOK_JOB_STARTED: guard.hook });
485
503
  logger.log(` HOME for ${org}: ${privateHome.home}`);
504
+ logger.log(` Job guard for ${org}: ${guard.hook}`);
486
505
 
487
506
  // Write the .cmd shortcut in the user's Startup folder so Explorer auto-runs
488
507
  // it at every interactive logon (Session 1). No Task Scheduler, no admin
@@ -518,10 +537,21 @@ async function startPreflight(options) {
518
537
  // an install that predates it, or a runner dir whose `.env` was replaced, has
519
538
  // no HOME line, and the job it picks up then dies inside `actions/checkout`.
520
539
  // Every registered dir gets it here, before a single listener comes up.
540
+ //
541
+ // And the job guard with it
542
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a box that
543
+ // installed before the guard existed, or a runner dir whose `.env` lost the
544
+ // line, would otherwise come online signing for anything. The hook script is
545
+ // rewritten from this package, so a framework update reaches the box on the
546
+ // next start; the allow lists are the operator's and are only appended to,
547
+ // and this path asks GitHub nothing, so `start` still works offline.
521
548
  const privateHome = ensureRunnerPrivateHome(home, { logger: log });
522
- for (const { org, dir } of listOrgRunnerDirs(home)) {
523
- ensureRunnerDirEnv(dir, { HOME: privateHome.home });
549
+ const dirs = listOrgRunnerDirs(home);
550
+ const guard = ensureRunnerJobGuard(home, { orgs: dirs.map(({ org }) => org), logger: log });
551
+ for (const { org, dir } of dirs) {
552
+ ensureRunnerDirEnv(dir, { HOME: privateHome.home, ACTIONS_RUNNER_HOOK_JOB_STARTED: guard.hook });
524
553
  log.log(`HOME for ${org}: ${privateHome.home}`);
554
+ log.log(`Job guard for ${org}: ${guard.hook}`);
525
555
  }
526
556
 
527
557
  const named = parseRunnerOrgs(env.OMEGA_RUNNER_ORGS);
@@ -966,7 +996,13 @@ async function removeRunnerHomeWithRetry(keepDirs, home) {
966
996
  // open by the tee while the removal runs.
967
997
  const envFile = runnerEnvFile(root);
968
998
  const logsDir = path.dirname(runnerLogFile(root));
969
- const keep = new Set([...(keepDirs || []), envFile, logsDir]);
999
+ // The job guard's two allow lists are configuration too
1000
+ // ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)): a list an
1001
+ // operator narrowed by hand outlives an uninstall exactly as the `.env` does.
1002
+ // The hook SCRIPT is install state and goes with the rest; the next install
1003
+ // writes it back from the package.
1004
+ const guardLists = jobGuardListFiles(root);
1005
+ const keep = new Set([...(keepDirs || []), envFile, logsDir, ...guardLists]);
970
1006
  const targets = (jetpack.list(root) || [])
971
1007
  .map((name) => path.join(root, name))
972
1008
  .filter((entry) => !keep.has(entry));
@@ -974,7 +1010,8 @@ async function removeRunnerHomeWithRetry(keepDirs, home) {
974
1010
  // Nothing kept survives on disk: the home itself goes too.
975
1011
  const kept = [...(keepDirs || [])].length > 0
976
1012
  || jetpack.exists(envFile) === 'file'
977
- || jetpack.exists(logsDir) === 'dir';
1013
+ || jetpack.exists(logsDir) === 'dir'
1014
+ || guardLists.some((file) => jetpack.exists(file) === 'file');
978
1015
  if (!kept) targets.push(root);
979
1016
 
980
1017
  for (const target of targets) {
@@ -1,6 +1,6 @@
1
1
  // Strategy-aware Windows code signer.
2
2
  //
3
- // Reads strategy from config platforms.win.signing.strategy:
3
+ // Reads strategy from config platforms.windows.signing.strategy:
4
4
  // self-hosted — sign with signtool against an EV USB token (typically on a self-hosted runner)
5
5
  // cloud — shell out to a cloud signing provider's CLI (Azure / SSL.com / DigiCert)
6
6
  // local — no-op (developer signs manually on their own Windows box)
@@ -33,6 +33,8 @@ runnerEnv.loadRunnerEnv();
33
33
  const attachLogFile = require('../utils/attach-log-file.js');
34
34
 
35
35
  const { startAutoUnlock } = require('../lib/sign-helpers/auto-unlock.js');
36
+ const { assertConsoleUnlocked } = require('../lib/sign-helpers/console-lock.js');
37
+ const { execWithLimit } = require('../lib/sign-helpers/exec-with-limit.js');
36
38
  const { writeUpdateInfo } = require('../lib/sign-helpers/update-info.js');
37
39
  const signEvents = require('../lib/sign-helpers/sign-events.js');
38
40
 
@@ -140,9 +142,9 @@ async function runSignCommand(options) {
140
142
  }
141
143
 
142
144
  if (strategy === 'cloud') {
143
- const provider = config.platforms?.win?.signing?.cloud?.provider;
145
+ const provider = config.platforms?.windows?.signing?.cloud?.provider;
144
146
  if (!provider) {
145
- throw new Error('strategy=cloud but no provider set (config platforms.win.signing.cloud.provider).');
147
+ throw new Error('strategy=cloud but no provider set (config platforms.windows.signing.cloud.provider).');
146
148
  }
147
149
  await signWithCloudProvider(provider, targets, inDir, outDir);
148
150
  return;
@@ -261,9 +263,17 @@ function buildVerifyCommand({ signtool, outPath }) {
261
263
  const SIGN_ATTEMPTS = 3;
262
264
  const SIGN_RETRY_DELAY_MS = 5000;
263
265
 
264
- // node-powertools' `execute` with `log: false` — what this file passes — rejects
265
- // with `new Error(stderr || 'Command failed with exit code N')`, and the sign
266
- // command ends in `2>&1` (it runs through a shell), so BOTH of signtool's
266
+ // A healthy sign takes ~18s (the timestamp round trip is most of it). One that
267
+ // is still running minutes later is waiting on something that will never come:
268
+ // a PIN typed at a locked console, a dialog nobody can see. Every attempt gets
269
+ // this hard limit, after which the signtool process tree is terminated and the
270
+ // attempt fails with a message that says so, instead of holding the job until
271
+ // GitHub's six-hour default cancels it ([#864](https://github.com/Omega-JS-Stack/omega/issues/864)).
272
+ const SIGN_TIME_LIMIT_MS = 3 * 60 * 1000;
273
+
274
+ // node-powertools' `execute` with `log: false` — what execWithLimit passes —
275
+ // rejects with `new Error(stderr || 'Command failed with exit code N')`, and the
276
+ // sign command ends in `2>&1` (it runs through a shell), so BOTH of signtool's
267
277
  // streams reach `e.message` and this is classifying on signtool's own words.
268
278
  //
269
279
  // Failures signtool NAMES as permanent. Retrying a wrong PIN walks the EV token
@@ -298,7 +308,7 @@ async function signWithRetry(cmd, options) {
298
308
  file,
299
309
  attempts = SIGN_ATTEMPTS,
300
310
  delayMs = SIGN_RETRY_DELAY_MS,
301
- exec = (c) => execute(c, { log: false }),
311
+ exec = (c) => execWithLimit(c, { limitMs: SIGN_TIME_LIMIT_MS, label: 'signtool' }),
302
312
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
303
313
  emit = signEvents.emit,
304
314
  startUnlock = () => ({ stop: () => {} }),
@@ -334,7 +344,17 @@ async function signWithRetry(cmd, options) {
334
344
  throw new Error(`signWithRetry exhausted ${attempts} attempts without a verdict`);
335
345
  }
336
346
 
337
- async function signWithSigntool(targets, inDir, outDir) {
347
+ // The signing loop. Both of its shell-outs and every line it prints sit behind
348
+ // one seam (`deps.exec` runs signtool, `deps.logger` prints), so the ORDER that
349
+ // makes the proof line true (sign, verify, THEN the line) is provable without a
350
+ // Windows box. Production passes neither: the real signtool and the real logger
351
+ // are the defaults, and the deliberate end-to-end sign is sign-windows-e2e.
352
+ async function signWithSigntool(targets, inDir, outDir, deps) {
353
+ deps = deps || {};
354
+ const exec = deps.exec;
355
+ const runVerify = exec || ((command) => execute(command, { log: false }));
356
+ const log = deps.logger || logger;
357
+
338
358
  const projectRoot = process.cwd();
339
359
  const { tokenRef, password, useThumbprint, signtool, timestampUrl } = resolveSigntoolEnv();
340
360
 
@@ -343,6 +363,11 @@ async function signWithSigntool(targets, inDir, outDir) {
343
363
  // update mechanism (not electron-updater) and don't need a yml.
344
364
  const signedExes = [];
345
365
 
366
+ // The PIN is typed into the Token Logon dialog on the interactive desktop.
367
+ // Behind a lock screen it never arrives and signtool waits forever, so a
368
+ // locked console is refused before the first call (console-lock.js).
369
+ if (useThumbprint && password) await assertConsoleUnlocked();
370
+
346
371
  for (const target of targets) {
347
372
  const rel = path.relative(inDir, target);
348
373
  const outPath = path.join(outDir, rel);
@@ -353,7 +378,7 @@ async function signWithSigntool(targets, inDir, outDir) {
353
378
 
354
379
  const cmd = buildSignCommand({ signtool, tokenRef, password, timestampUrl, outPath });
355
380
 
356
- logger.log(`Signing ${path.relative(projectRoot, outPath)}${useThumbprint ? ' (thumbprint mode)' : ''}...`);
381
+ log.log(`Signing ${path.relative(projectRoot, outPath)}${useThumbprint ? ' (thumbprint mode)' : ''}...`);
357
382
 
358
383
  const fileBytes = (() => { try { return fs.statSync(outPath).size; } catch (_) { return null; } })();
359
384
  signEvents.emit('sign-start', {
@@ -370,6 +395,7 @@ async function signWithSigntool(targets, inDir, outDir) {
370
395
  try {
371
396
  await signWithRetry(cmd, {
372
397
  file: path.basename(outPath),
398
+ exec,
373
399
  startUnlock: () => (useThumbprint ? startAutoUnlock({ password, logger }) : { stop: () => {} }),
374
400
  });
375
401
  } catch (e) {
@@ -385,7 +411,7 @@ async function signWithSigntool(targets, inDir, outDir) {
385
411
  // Verify is a local check with no third party in it — one shot.
386
412
  const verifyCmd = buildVerifyCommand({ signtool, outPath });
387
413
  try {
388
- await execute(verifyCmd, { log: false });
414
+ await runVerify(verifyCmd);
389
415
  } catch (e) {
390
416
  signEvents.emit('sign-fail', {
391
417
  file: path.basename(outPath),
@@ -399,7 +425,11 @@ async function signWithSigntool(targets, inDir, outDir) {
399
425
  file: path.basename(outPath),
400
426
  duration_ms: Date.now() - fileStart,
401
427
  });
402
- logger.log(logger.format.green(`✓ Signed: ${path.relative(projectRoot, outPath)}`));
428
+ // The proof line, in the mac lane's shape (hooks/notarize.js): the verify
429
+ // above aborts the job on a failure, so this line names the check that ran
430
+ // rather than claiming a signature nobody checked
431
+ // ([#918](https://github.com/Omega-JS-Stack/omega/issues/918)).
432
+ log.log(`Signed and verified: signtool verify /pa accepts ${path.basename(outPath)}.`);
403
433
 
404
434
  if (outPath.toLowerCase().endsWith('.exe')) {
405
435
  signedExes.push({ filePath: outPath, urlName: path.basename(outPath) });
@@ -414,9 +444,9 @@ async function signWithSigntool(targets, inDir, outDir) {
414
444
  const pkg = Manager.getPackage('project') || {};
415
445
  const version = pkg.version;
416
446
  if (!version) {
417
- logger.warn('Could not determine version from package.json — skipping latest.yml generation.');
447
+ log.warn('Could not determine version from package.json; skipping latest.yml generation.');
418
448
  } else {
419
- logger.log(`Generating Windows auto-updater feed (latest.yml) for ${signedExes.length} installer(s)...`);
449
+ log.log(`Generating Windows auto-updater feed (latest.yml) for ${signedExes.length} installer(s)...`);
420
450
  try {
421
451
  await writeUpdateInfo({
422
452
  signedExes,
@@ -428,8 +458,8 @@ async function signWithSigntool(targets, inDir, outDir) {
428
458
  // Don't fail the whole sign step if yml generation hits something unexpected —
429
459
  // the signed exe is still valid + uploadable. But this IS a real problem
430
460
  // because auto-updates won't work, so log loudly.
431
- logger.error(`latest.yml generation failed: ${e.message}`);
432
- logger.error(' Signed binary is OK but Windows auto-updater will not see this release.');
461
+ log.error(`latest.yml generation failed: ${e.message}`);
462
+ log.error(' Signed binary is OK but Windows auto-updater will not see this release.');
433
463
  }
434
464
  }
435
465
  }
@@ -554,6 +584,7 @@ module.exports.signWithRetry = signWithRetry;
554
584
  module.exports.isTransientSignFailure = isTransientSignFailure;
555
585
  module.exports.SIGN_ATTEMPTS = SIGN_ATTEMPTS;
556
586
  module.exports.SIGN_RETRY_DELAY_MS = SIGN_RETRY_DELAY_MS;
587
+ module.exports.SIGN_TIME_LIMIT_MS = SIGN_TIME_LIMIT_MS;
557
588
 
558
589
  // Exports for the Windows-gated end-to-end suite — the halves that shell out to
559
590
  // signtool for real. They only run where signtool and the EV token are