@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
@@ -14,70 +14,143 @@ framework dists like every devkit module.
14
14
  | Export | Contract |
15
15
  |--------|----------|
16
16
  | `parseRemoteUrl(url)` | ssh / https / `.git`-less GitHub remotes → `{ owner, repo }` (non-GitHub → null) |
17
- | `dispatchRepo(config)` | `{ owner, repo }` from the BRAND's config (`@omega.js/config`'s `brandRepo`), throwing on half an address: the one address every deploy dispatch uses ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)) |
18
- | `resolveRepo({ cwd, remote, execFn })` | `{ owner, repo }` from `git config --get remote.origin.url` (the direct lane's slug fallback, never a dispatch address) |
17
+ | `dispatchRepo(config)` | `{ owner, repo }` from the BRAND's config (`@omega.js/config`'s `sourceRepo`: `<brand.id>-omega` under `repo.org`), throwing on half an address: the one address every deploy dispatch uses ([#799](https://github.com/Omega-JS-Stack/omega/issues/799), [#883](https://github.com/Omega-JS-Stack/omega/issues/883)) |
18
+ | `dispatchTarget({ projectRoot, config, workflow })` | `{ owner, repo, workflow }`: the repo above plus the workflow file the target's scaffold actually wrote (`composedWorkflowName`, composed in a brand and plain standalone). The WHOLE dispatch address, one helper for all four verbs ([#847](https://github.com/Omega-JS-Stack/omega/issues/847)) |
19
+ | `resolveRepo({ cwd, remote, execFn })` | `{ owner, repo }` from `git config --get remote.origin.url`: the repo a working tree SITS IN, for the lane's own bookkeeping. Never a dispatch address, and since [#883](https://github.com/Omega-JS-Stack/omega/issues/883) never a publish address either |
19
20
  | `resolveToken({ env, execFn })` | `GH_TOKEN` → `GITHUB_TOKEN` → `gh auth token` → null |
21
+ | `gitAuthEnv(token)` / `scrubToken(message, token)` (`@omega.js/devkit/git-auth`) | how a token reaches `git` for ONE push and how it is kept out of anything printed: the `GIT_CONFIG_COUNT` trio carrying `http.https://github.com/.extraheader` in the BASIC form (`x-access-token:<token>`, base64, the form the git endpoints accept), and the redaction of both that form and the raw token from a rethrown failure. No token, no entries at all. BOTH pushes that address a repo their checkout is not authenticated for use it: the snapshot push and the web deploy's gh-pages push ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)), so a credential can never ride an argv, a url or an error message in either |
20
22
  | `buildDispatch({ owner, repo, workflow, ref, inputs })` | the PLAN: `{ method, url, body, runsUrl }` — dry-run output is exactly this |
21
23
  | `dispatchWorkflow(plan, { token, fetchFn })` | POST to `/repos/{o}/{r}/actions/workflows/{wf}/dispatches`; 204 = accepted, anything else throws with status + body |
22
- | `deployViaDispatch({ workflow, cwd, owner, repo, ref, inputs, dryRun, … })` | the one path: resolve → plan → (`dryRun` returns the plan unsent) → dispatch |
23
- | `assertNoLocalSpecs({ dir })` | the TREE-WIDE `file:` guard: throws (listing every offender) when any target in the brand carries a `file:` @omega.js spec — one linked sibling breaks the whole CI install (cp194) |
24
- | `syncWorkingTree({ cwd, message, logger })` | plain-git commit + push before a dispatch (`git add -A` → commit only when staged → push; argument arrays, no shell) — pushes trigger NOTHING (D13) |
25
-
26
- `ref` defaults to `main`; pass the real branch when it isn't (the admin post
27
- routes read the repo's `default_branch` first). All I/O is injectable
28
- (`fetchFn`/`execFn`) — pinned by `packages/devkit/test/deploy.test.js`.
29
-
30
- **Every framework's deploy verb dispatches to the BRAND repo named by its config, never the git remote** ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)): web, extension and desktop (`omega release` included) all take `{ owner, repo }` from `dispatchRepo(config)` and refuse when the config names none, because a remote answers the repo the working tree sits in, which inside a brand nested in another repo is the enclosing one.
24
+ | `deployViaDispatch({ workflow, dir, owner, repo, inputs, dryRun, … })` | the one path, and the ORCHESTRATOR of the lane: resolve the lane, build the plan (`dryRun` returns it unsent, with the workflow-file plan printed beside it), then check, compose, pack, push and restore as the lane needs, wait until GitHub lists the workflow, dispatch |
25
+ | `deliverLane({ lane, owner, repo, ref, token, message, logger, steps })` | the DELIVERY half of the lane, in one place for its two callers ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): read the repo's default branch ONCE and heal a `gh-pages` one back to `main` right there ([#922](https://github.com/Omega-JS-Stack/omega/issues/922)), refuse a checkout behind it, push the composed workflow files to it when they differ, pack, force-push the brand folder to `omega-deploy` and restore the tree, returning `{ sha }`. A brand outside git delivers nothing and returns `{ sha: null }`. `deployViaDispatch` calls it for one target, and the brand-root fan-out calls it once for a whole run |
26
+ | `resolveDeployLane({ dir, execFn })` | the LANE every deploy takes, one derivation for all four targets ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)) and ONE mode since [#915](https://github.com/Omega-JS-Stack/omega/issues/915): `{ mode, ref, nested, linked, repo, brandRoot }`, mode `snapshot` and ref `omega-deploy` for every brand with a git repo (`nested` = the brand root is not the toplevel of its git repo and `linked` = the brand tree carries any `file:` @omega.js spec ride ON the lane, because the steps read them). A brand outside git is mode `dispatch`: nothing to snapshot from, so the wait and the dispatch are the whole lane. A linked brand with no git repo at all is REFUSED by name: a snapshot needs an index to build in |
27
+ | `SNAPSHOT_REF` | `omega-deploy`, the one branch CI ever builds, exported so no consumer types it (the backend's admin publish reads it from here, [#919](https://github.com/Omega-JS-Stack/omega/issues/919)) |
28
+ | `assertNotBehind({ cwd, branch, logger, execFn })` | the refusal a local deploy owes the admin publish ([#915](https://github.com/Omega-JS-Stack/omega/issues/915)): `git fetch origin <default>` then `git merge-base --is-ancestor origin/<default> HEAD`, throwing with `git pull` in the line when the checkout is behind, because the force-push would otherwise deploy away what the branch carries. No `origin`, or no such branch yet, is nothing to be behind |
29
+ | `@omega.js/devkit/deploy-snapshot` (`pushSnapshot`, `pushWorkflowFiles`, `composedWorkflowFiles`, `defaultBranchOf`, `healDefaultBranch`, `waitForWorkflow`, `waitForRef`, `ghHeaders`) | the network steps the orchestrator calls: force-push the brand folder to `<owner>/<repo>` at `ref` out of a TEMPORARY git index, push the composed workflows to the DEFAULT branch through the git data api when they differ, read that branch's name (one spelling, shared by every step that needs it), HEAL a `gh-pages` default back to `main` on the name that read returned ([#922](https://github.com/Omega-JS-Stack/omega/issues/922)), poll until GitHub REGISTERS the composed workflow (read from the default branch), and poll until the ref RESOLVES to the pushed sha, because a dispatch sent in the same second as a force-push can resolve to the previous commit ([#902](https://github.com/Omega-JS-Stack/omega/issues/902)) |
30
+
31
+ `ref` defaults to `main` in `buildDispatch` alone; every real deploy passes the
32
+ lane's, which is `omega-deploy` ([#915](https://github.com/Omega-JS-Stack/omega/issues/915)).
33
+ All I/O is injectable (`fetchFn`/`execFn`), pinned by
34
+ `packages/devkit/test/deploy.test.js`.
35
+
36
+ **ONE branch, three facts** ([#915](https://github.com/Omega-JS-Stack/omega/issues/915),
37
+ Ian 2026-09-14: "it just always runs the ci deploy from a dedicated deploy
38
+ branch and then no matter who starts the deploy, we just get the relevant files
39
+ to the deploy branch"). (1) **CI only ever builds `omega-deploy`**: every brand,
40
+ nested or not, linked or not, force-pushes its folder there and dispatches that
41
+ ref, and the admin publish writes its posts there too ([#919](https://github.com/Omega-JS-Stack/omega/issues/919)).
42
+ (2) **A default branch receives the composed workflow FILES and nothing else**,
43
+ in a `chore(ci): compose <names>` commit made through the git data api, and only
44
+ when a file is missing or differs; the reason is GitHub's own rule that a
45
+ workflow is registered from the default branch. The developer's tree is never
46
+ committed by a deploy: the old sync (`git add -A`, message `Deploy`, push) is
47
+ gone, and so is `--no-sync` with it. (3) **A local deploy REFUSES when its
48
+ checkout is behind** that branch, naming `git pull`, because the force-push
49
+ would otherwise deploy away a post the admin published while this laptop was
50
+ stale.
51
+
52
+ **Every framework's deploy verb dispatches to the BRAND repo named by its config, never the git remote** ([#799](https://github.com/Omega-JS-Stack/omega/issues/799)): all four targets (`omega release` included) take `{ owner, repo }` from `dispatchRepo(config)` and refuse when the config names none, because a remote answers the repo the working tree sits in, which inside a brand nested in another repo is the enclosing one. **The one ADDRESS helper is `dispatchTarget({ projectRoot, config, workflow })`** ([#847](https://github.com/Omega-JS-Stack/omega/issues/847)): repo plus composed workflow name in one call, which desktop kept a local copy of and its three siblings composed by hand.
31
53
 
32
54
  ## Scaffolded workflows — no push triggers
33
55
 
34
56
  | Target | Workflow | Triggers | Publishes |
35
57
  |--------|----------|----------|-----------|
36
- | web | `.github/workflows/build.yml` (authoritative, regenerated by the target scaffold every verb runs) | `workflow_dispatch` + `repository_dispatch: [omega-deploy]` | gh-pages |
37
- | extension | `.github/workflows/publish.yml` (authoritative via defaults engine) | same | stores (when creds present) + the package zip attached to a GitHub release `extension-v<version>` (D9 addendum 2 — durable artifact channel, no 7-day purge) |
38
- | desktop | `.github/workflows/build.yml` | `workflow_dispatch` (was already manual-only — the reference shape) | GitHub releases in the brand's ONE public releases repo (`<brand.id>-releases` by default, addressed by `@omega.js/config`'s `releasesRepo`, [#799](https://github.com/Omega-JS-Stack/omega/issues/799)): electron-builder's publish block, the signed-Windows upload and the autoupdater feed all name it |
39
- | backend | none | — | direct `firebase deploy` from the CLI |
58
+ | web | `.github/workflows/build.yml` (authoritative, regenerated by the target scaffold every verb runs) | `workflow_dispatch` | `gh-pages` on the target's OWN WEBSITE REPO, `<brand.id>-<target name>` under `repo.org` ([#883](https://github.com/Omega-JS-Stack/omega/issues/883), `@omega.js/config`'s `websiteRepo`): the job runs the framework's own `omega deploy --direct`, so CI and a hand deploy take one code path, and no workflow input names a repo |
59
+ | extension | `.github/workflows/publish.yml` (authoritative via defaults engine) | same | the stores the brand DECLARES ([#867](https://github.com/Omega-JS-Stack/omega/issues/867): a declared store with no credential refuses, one whose listing does not exist yet prints the manual step) + the package zips attached to a GitHub release `<target name>-v<version>` (`extension-v<version>` for the default name) in the brand's ONE public releases repo, `<brand.id>-releases` ([#883](https://github.com/Omega-JS-Stack/omega/issues/883); D9 addendum 2: durable artifact channel, no 7-day purge). The release is made by the publish TASK, in node through the devkit `gh` wrapper, so the workflow types no repo name and carries the brand's cross-repo `GH_TOKEN`, never `secrets.GITHUB_TOKEN` (the extension is a declared consumer of that key in the env schema, delivery `ci`, so its own secrets step arms the repo with it rather than waiting for a sibling target's). It runs right after the zip check and BEFORE the store gate: the releases repo is the channel the brand owns, so a brand with no store credentials, or one failed store upload, still publishes its zips. A FIRST Firefox publish (no `targets.<name>.listings.firefox.id`) also signs with `--amo-metadata` ([#884](https://github.com/Omega-JS-Stack/omega/issues/884)): AMO will not create a listing without a summary (`brand.description`, cut to 250), categories (`targets.extension.categories`, default `['alerts-updates']`) and a license (the target `package.json` `license`, `UNLICENSED` listing as `all-rights-reserved`), so the deploy that creates the listing carries them and every update carries none. The id the listing is created under is not the store's gift: it is the brand's own derived value, pinned into the brand config as `targets.<name>.listings.firefox.id` by the extension's LOCAL scaffold before the snapshot is pushed, and the runner writes no config at all ([#893](https://github.com/Omega-JS-Stack/omega/issues/893)). The three store listing IDS are config, not repo secrets: only the store API credentials ride the secrets block |
60
+ | desktop | `.github/workflows/build.yml` | `workflow_dispatch` (carrying the `platforms` input), the same one trigger its three siblings carry ([#880](https://github.com/Omega-JS-Stack/omega/issues/880): the manual-only holdout, converged; [#923](https://github.com/Omega-JS-Stack/omega/issues/923): narrowed to the single event, below) | GitHub releases in the brand's ONE public releases repo, always `<brand.id>-releases` under `repo.org` (`@omega.js/config`'s `releasesRepo`, [#799](https://github.com/Omega-JS-Stack/omega/issues/799); the owner/repo overrides are retired, [#883](https://github.com/Omega-JS-Stack/omega/issues/883)): electron-builder's publish block, the signed-Windows upload and the autoupdater feed all name it, and the deploy precheck provisions it public through the devkit `ensureRepo`. Each published asset is linked from the site at `/download/<platform>/<format>` (`/download/mac/dmg`, `/download/windows/nsis`, `/download/linux/deb`, [#867](https://github.com/Omega-JS-Stack/omega/issues/867)); the segments those three replaced (`mac/universal`, `windows/universal`, `linux/debian`) keep their pages as redirects to the same file, and `/download/linux/snap` goes to the brand's Snap Store listing |
61
+ | backend | `.github/workflows/deploy.yml` (template at `packages/backend/src/defaults/.github/workflows/deploy.yml`, composed into the brand root as `backend-deploy.yml` like the other three) | `workflow_dispatch` | the Firebase deploy the runner performs with `omega deploy --direct` (the framework bin by path, below) (checkout, setup-node, the firewall step, `sfw npm install`, the target `.env` written from the generated secrets block, the service-account file, the Firebase CLI, `gcloud` authenticated with that file, then the verb) |
40
62
 
41
63
  Existing consumers converge on their next omega verb (both scaffold
42
64
  engines treat workflow files as framework-owned overwrites).
43
65
 
44
66
  **The desktop workflow runs no tests** ([#802](https://github.com/Omega-JS-Stack/omega/issues/802)): CI is dispatch-only and BUILDS. Its Test job (first ahead of Build, then beside it) was a leftover of the era before that standard, and it is gone: `build` needs `setup`, and `finalize`, the job that flips the draft release to published, needs `[setup, build, windows-sign]` and gates on `needs.build.result == 'success'` inside its `always()`. Proof lives where web's and the extension's release workflows already leave it: the suites run on the developer's machine, and the commit gate runs the battery at ship ([testing.md](testing.md)).
45
67
 
46
- **In a brand monorepo the workflow moves to the ROOT** ([#265](https://github.com/Omega-JS-Stack/omega/issues/265)): GitHub executes workflows from the repo root's `.github/workflows/` only, so a `targets/<target>/.github/workflows/*.yml` never fires — CI builds and store publishes silently do not exist. The target scaffold composes the target's workflow into the brand root as `<target>-<workflow>.yml` (`extension-publish.yml`) via `@omega.js/devkit/ci-workflows`: every post-checkout `run:` step scoped to the target (a per-step `working-directory:`, never a workflow-level `defaults.run.working-directory` — that would also scope the steps running before `actions/checkout`, where the target dir does not exist yet, killing the job on step 1), the path-bearing inputs of a post-checkout `uses:` step rewritten to the target dir (an action ignores `working-directory:` — the table is explicit per action: `actions/cache`/`upload-artifact`/`download-artifact` `path`, `peaceiris/actions-gh-pages` `publish_dir`, and every `hashFiles()` pattern, which globs from the workspace root wherever it sits), a per-target concurrency group, regenerated from the template on every verb (a rerun updates that one file, never duplicates), and the target's own dead copy swept when the framework wrote every line of it: an exact match, or a copy a SUPERSEDED template wrote, which is what templates evolve into by GAINING lines ([#334](https://github.com/Omega-JS-Stack/omega/issues/334)); a copy carrying a line no current template ships is kept with a loud move-it-then-delete warning, never destroyed (accepted blind spot: an edit that ONLY deletes template lines is still swept — harmless, since a target-dir workflow never runs). `omega deploy` dispatches the composed name in a brand and the plain name standalone, and so does desktop's `omega release`: all three frameworks take the file name from ONE helper, `composedWorkflowName({ targetDir, brandRoot, workflow })` in `@omega.js/devkit/ci-workflows` (desktop joined it in [#799](https://github.com/Omega-JS-Stack/omega/issues/799), which also moved its dispatch repo off the git remote onto the config). Wired for all three: extension (`extension-publish.yml`), web (`website-build.yml`), and desktop (`desktop-build.yml`, composed by its ensure-target pass since [#627](https://github.com/Omega-JS-Stack/omega/issues/627)); each carries its schema-rendered secrets block. Scoping is by working directory, not a `paths:` filter — dispatch-only workflows have nothing to path-filter.
68
+ **In a brand monorepo the workflow moves to the ROOT** ([#265](https://github.com/Omega-JS-Stack/omega/issues/265)): GitHub executes workflows from the repo root's `.github/workflows/` only, so a `targets/<target>/.github/workflows/*.yml` never fires: CI builds and store publishes silently do not exist. The target scaffold composes the target's workflow into the brand root as `<target>-<workflow>.yml` (`extension-publish.yml`) via `@omega.js/devkit/ci-workflows`: every post-checkout `run:` step scoped to the target (a per-step `working-directory:`, never a workflow-level `defaults.run.working-directory`: that would also scope the steps running before `actions/checkout`, where the target dir does not exist yet, killing the job on step 1), the path-bearing inputs of a post-checkout `uses:` step rewritten to the target dir (an action ignores `working-directory:`; the table is explicit per action: `actions/cache`/`upload-artifact`/`download-artifact` `path`, and every `hashFiles()` pattern, which globs from the workspace root wherever it sits), a per-target concurrency group, regenerated from the template on every verb (a rerun updates that one file, never duplicates), and the target's own dead copy swept when the framework wrote every line of it: an exact match, or a copy a SUPERSEDED template wrote, which is what templates evolve into by GAINING lines ([#334](https://github.com/Omega-JS-Stack/omega/issues/334)); a copy carrying a line no current template ships is kept with a loud move-it-then-delete warning, never destroyed (accepted blind spot: an edit that ONLY deletes template lines is still swept, harmless, since a target-dir workflow never runs). `omega deploy` dispatches the composed name in a brand and the plain name standalone, and so does desktop's `omega release`: all four verbs take the file name from ONE helper, `composedWorkflowName({ targetDir, brandRoot, workflow })` in `@omega.js/devkit/ci-workflows`, reached through `dispatchTarget` ([#847](https://github.com/Omega-JS-Stack/omega/issues/847)) (desktop joined it in [#799](https://github.com/Omega-JS-Stack/omega/issues/799), which also moved its dispatch repo off the git remote onto the config). Wired for all FOUR: extension (`extension-publish.yml`), web (`web-build.yml`), desktop (`desktop-build.yml`, composed by its ensure-target pass since [#627](https://github.com/Omega-JS-Stack/omega/issues/627)) and backend (`backend-deploy.yml`, [#872](https://github.com/Omega-JS-Stack/omega/issues/872)); each carries its composed secrets block (the env schema plus the target's composed production values, below). Scoping is by working directory, not a `paths:` filter: dispatch-only workflows have nothing to path-filter.
69
+
70
+ **The firewall step is written ONCE, in the composer** ([#871](https://github.com/Omega-JS-Stack/omega/issues/871), [#872](https://github.com/Omega-JS-Stack/omega/issues/872)): every template carries the token `{{ installFirewall }}` on its own line, indented as a list item where the step belongs, and `composeWorkflow` renders it into the pinned Socket action (`uses: SocketDev/action@v1.3.2` with `mode: firewall-free`, the one `FIREWALL_ACTION` constant in `@omega.js/devkit/ci-workflows`). No template names the action or its version, so pinning a new one is a single edit in devkit and every brand picks it up on its next verb. Each framework's OWN scaffold renders the token too, through the same `renderInstallFirewall` (the one renderer, exported beside `composeWorkflow` and called by all four): a standalone target composes nothing, so without that pass the raw `{{ installFirewall }}` would reach the written file. The render is TWO steps, not one ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): the action caches its Windows binary under the extension-less name `sfw`, which cmd.exe cannot execute, so a following `if: runner.os == 'Windows'` step, itself under `shell: cmd` because a self-hosted Windows box has no bash on the runner's PATH, copies the path the action reports (`steps.omega-firewall.outputs.firewall-path-binary`) to `sfw.exe`, and a job forcing `shell: cmd` (desktop's windows legs do) finds it on the same PATH. A cmd-shelled install also spells the command `sfw npm.cmd ci`: the firewall spawns what it is given literally, with no PATHEXT resolution, so bare `npm` there is `Command 'npm' not found in PATH`. The one install with no firewall at all is the desktop template's self-hosted Windows signing job: that box is the brand owner's own machine, its network does not reach Socket's API (the firewall fails closed on `fetch failed`), and the lockfile it installs is the one the hosted build jobs of the same run already installed through the firewall.
71
+
72
+ **A workflow runs the framework's own `bin/omega` FILE, by path** ([#877](https://github.com/Omega-JS-Stack/omega/issues/877), replacing the `npx --no-install omega-<framework>` form [#872](https://github.com/Omega-JS-Stack/omega/issues/872) introduced): every template spells it `node "${{ github.workspace }}/node_modules/@omega.js/<framework>/bin/omega" <verb>`, and no template runs `npx` at all. On a runner the shared `omega` bin link is an install-order accident, and a bare `npx omega` that finds nothing linked falls through to the npm REGISTRY: the playground's first backend deploy fetched a stranger's package called `omega` and ran it with every secret in the job env, hanging the version step for 30 minutes until the run was cancelled. The four `omega-<framework>` bins that answered that are gone with it (a human types only `omega`), so nothing resolves a bin NAME on a runner: the workflow belongs to exactly one framework and runs that framework's file. The path is anchored at `github.workspace` rather than the step's own dir because npm hoists a workspace's dependencies to the ROOT of the checkout, where a target-scoped `./node_modules` never sees them. That same run is why the backend job sets no `NODE_ENV`: npm skips devDependencies under `NODE_ENV=production`, so the brand's own `@omega.js/manager` and every framework a brand declares as a devDependency never installed, which is what left the bin unlinked (the deploy never reads the variable anyway, `ensureStaged` composes the production env from its own `environment: 'production'` option). **And each job installs only its OWN workspace** ([#898](https://github.com/Omega-JS-Stack/omega/issues/898)): every template carries the `{{ installWorkspace }}` token at the end of its install command, which composition renders into `--workspace .` (the dot is the target dir itself, since every composed run step already carries `working-directory: targets/<name>` and npm resolves `--workspace` against the cwd) and a standalone scaffold renders into nothing, because a standalone target is its own repo root and declares no workspaces. So no target's `engines.node` pin is installed under another job's Node: the desktop job on Electron's Node no longer installs the backend workspace pinned to the Firebase runtime. All three rules are pinned across the four templates by `packages/devkit/test/ci-workflows.test.js`.
73
+
74
+ **All four templates pin the SAME actions** ([#880](https://github.com/Omega-JS-Stack/omega/issues/880)): `actions/checkout@v7` and `actions/setup-node@v7` on every one of the four, and the family major for each of the rest wherever a template reaches for one (`actions/upload-artifact@v7`, `actions/download-artifact@v8`, `actions/cache@v6`), so an action upgrade is one edit for the family rather than four to remember, and the same test file pins both halves. A checkout is `fetch-depth: 1` wherever no job reads git HISTORY, which today is everywhere: nothing in a desktop release reads history (electron-builder publishes over the API), and the extension build's `git config` and `git archive HEAD` are both answered by a depth-1 clone. Each template also carries the sibling header (the deliberate-deploys framing plus the regenerated-by-ensureTarget line), a version-logging step, and a `timeout-minutes` on every job, so no run can hold a runner for GitHub's six-hour default.
47
75
 
48
- **The monorepo runs the playground's desktop release itself** ([#802](https://github.com/Omega-JS-Stack/omega/issues/802)): the playground brand is a DIRECTORY inside this repo, so the file its desktop target composes at `brands/omega-playground/.github/workflows/desktop-build.yml` sits where GitHub never looks, and the desktop release run (hosted build, the self-hosted EV-token Windows signer registered to the Omega-JS-Stack org, finalize) had no repo to execute in. It runs in a repo of its OWN: `Omega-JS-Stack/omega-playground-rehearsal`, PRIVATE, throwaway, `<brand.id>-rehearsal` per the brand repo rule ([#808](https://github.com/Omega-JS-Stack/omega/issues/808)). Not a branch of this repo and not the playground's own declared repo, for reasons that leave no other shape: GitHub dispatches a workflow only when it exists on the target repo's DEFAULT branch (a `rehearsal` branch answered HTTP 404 until the file went to `main`), the org's runner group refuses a public repo, and `Omega-JS-Stack/omega-playground` is public because it serves gh-pages, so a snapshot there would publish the whole framework source. Nothing rehearsal-shaped lives on omega's own branches, and `main` moves only when Ian ships it. The workflow itself, `.github/workflows/playground-desktop.yml`, is rendered from the SAME desktop template through the same `composeWorkflow` by `scripts/playground-desktop-workflow.js` (target path `brands/omega-playground/targets/desktop`), with the two monorepo-only edits a brand never needs: a root `npm install && npm run prepare --workspaces --if-present` ahead of the target steps in every job that checks out, because the playground's `@omega.js/*` deps are `file:` links into this checkout, and the target's `npm ci` becoming `npm install`, because the lockfile `npm ci` would read is the brand root's and that file is not committed. Neither install is `npm ci`: the root one runs on a SNAPSHOT of a working tree, whose root lockfile may lag its package.json files until the ship's own `npm install` heals it, and `npm ci` refuses a lagging lockfile. That rendered file is GITIGNORED here and committed nowhere: it is rewritten before every rehearsal and lives only in the snapshot repo, so `scripts/playground-desktop-workflow.test.js` (in the root `test:packages` lane) checks the RENDER against the template and drives the generator to prove it writes exactly that. The run is TEST-ONLY and never fires on a push: one command rehearses it, `npm run rehearse:desktop -- --platforms windows [--watch]` (`scripts/playground-desktop-rehearsal.js`), because a workflow only runs from code GitHub already has. Six steps: render the workflow; ensure the rehearsal repo exists (created private the first time); PUBLISH the playground desktop target's composed secrets to it (the set `omega push-secrets` composes for a brand, the env schema's desktop delivery keys valued from the target's `.env` cascade through desktop's own file resolver, so `APPLE_API_KEY` still travels as base64, sent here because push-secrets publishes to the brand's DECLARED repo and skips loudly on the remote mismatch a target nested in this monorepo always is, which is why the first real run died inside `npm run package` on a missing `GOOGLE_ANALYTICS_SECRET`); commit the WORKING TREE (untracked-not-ignored files included, the ignored workflow file force-added, the playground's uncommitted lockfile excluded) out of a TEMPORARY index (`GIT_INDEX_FILE`, so no stash, no checkout, and the working tree, the real index and every local branch untouched) onto the local ref `refs/rehearsal/head`, which is outside `refs/heads/` so no branch appears here at all, and force-push that commit to the rehearsal repo's `main`; wait for GitHub to register the pushed workflow, then dispatch it there; then print the run's URL and follow it with `--watch`. Nothing is `gh secret set` by hand. The dispatch WAITS for the push to register instead of asking a human for a second attempt: a dispatch reads the workflow off the repo's DEFAULT branch and GitHub indexes a freshly pushed one a few seconds after it arrives, so the first run against the brand-new repo answered `HTTP 404: workflow playground-desktop.yml not found on the default branch` to both the run probe and the dispatch, while the same command a minute later worked. The step now polls `gh api repos/<repo>/actions/workflows/<file>` up to 20 times, 3 seconds apart, one line per try, and fails loudly past that budget (a push that never carried the file, or a repo whose default branch is not `main`). Re-running with no edits reuses the same commit and pushes nothing new. A real brand is its own repo and needs none of this; it gets the composed file at its own root from any omega verb.
76
+ **The playground deploys like any other brand** ([#872](https://github.com/Omega-JS-Stack/omega/issues/872), retiring the [#802](https://github.com/Omega-JS-Stack/omega/issues/802) rehearsal repo): the playground is a DIRECTORY inside this monorepo, so the workflows it composes at `brands/playground-omega/.github/workflows/` sit where GitHub never looks, and the run had nowhere to execute. That is exactly what a NESTED brand is, and the one lane answers it for every brand shaped that way: `npx omega deploy` in `brands/playground-omega/targets/desktop` force-pushes the brand folder to `Omega-JS-Stack/playground-omega` at `omega-deploy` ([#915](https://github.com/Omega-JS-Stack/omega/issues/915): the mirror repo's `main` now holds the composed workflow files and nothing else, because that is where GitHub registers them from), waits for GitHub to list `desktop-build.yml` there, and dispatches it against the deploy branch. Nothing of the monorepo's own history goes with the snapshot. The private `playground-rehearsal` repo, the two `scripts/playground-desktop-*.js` generators, their tests and the `npm run rehearse:desktop` command are all gone: the playground gets no treatment a brand does not, and the thing that used to be rehearsed is now the lane itself.
49
77
 
50
- **A web deploy fills its own base path** ([#358](https://github.com/Omega-JS-Stack/omega/issues/358)): `brand.url` set → the site serves at that domain's root (the CNAME both lanes publish cannot carry a path) → `OMEGA_PATH_PREFIX=/`; unset → the default project address `https://<owner>.github.io/<name>/` → `/<name>/`, from the same repo slug the direct plan resolves. `--direct` sets it around the build it runs itself, and the scaffolded `build.yml` derives it remotely through the SAME function (`@omega.js/web/deploy`'s `targetPathPrefix()`, `GITHUB_REPOSITORY` naming the repo when the checked-out config carries no slug) into `$GITHUB_ENV`. An explicitly exported `OMEGA_PATH_PREFIX` wins (publisher machinery like workkit supplies its own); `omega dev` and a bare `omega build` stay at the root ([#355](https://github.com/Omega-JS-Stack/omega/issues/355)).
78
+ **A web deploy fills its own base path** ([#358](https://github.com/Omega-JS-Stack/omega/issues/358)): the target url set → the site serves at that domain's root (the CNAME both lanes publish cannot carry a path) → `OMEGA_PATH_PREFIX=/`; unset → the default project address `https://<owner>.github.io/<brand.id>-<target>/` → `/<brand.id>-<target>/`, from the same WEBSITE REPO the direct plan pushes to. `--direct` sets it around the build it runs itself, and CI runs that same lane; the scaffolded `build.yml` also prints it through the SAME function (`@omega.js/web/deploy`'s `targetPathPrefix()`) into `$GITHUB_ENV` before the step that consumes it. `GITHUB_REPOSITORY` names the repo the run CHECKED OUT (the source repo), so it names nothing here. An explicitly exported `OMEGA_PATH_PREFIX` wins (publisher machinery like workkit supplies its own); `omega dev` and a bare `omega build` stay at the root ([#355](https://github.com/Omega-JS-Stack/omega/issues/355)).
51
79
 
52
- **The direct lane publishes BOTH Pages shapes** ([#361](https://github.com/Omega-JS-Stack/omega/issues/361)): with a `brand.url` naming a domain of the brand's own it writes the CNAME and reports the custom domain; without one it deploys a PROJECT site — the address derived from the repo slug (`https://<owner>.github.io/<name>/`), the build mounted under the matching base path, and the CNAME step skipped entirely (an empty CNAME file would claim a domain). Plan and prefix come from ONE derivation, so the address a deploy reports and the path its build mounts under can never disagree. The slug itself resolves config first, then `GITHUB_REPOSITORY`, then the working tree's own `origin` remote — a brand cloned without a slug in its config still names its repo. The plan refuses only when nothing names the repo at all, and says so, naming only what actually resolves it: the config keys, the environment, the remote, or the CI dispatch lane. A project site still SETS `brand.url` — to its Pages URL, no trailing slash (`https://<owner>.github.io/<name>`) — because absolute URLs (canonical, `og:url`, hreflang alternates, the sitemap) build from it, and nothing derives it at build time; the direct lane WARNS on a project-site plan with `brand.url` unset, printing the exact value to set ([#366](https://github.com/Omega-JS-Stack/omega/issues/366)). **A `*.github.io` host is never read as a custom domain**, anywhere that reasons from `brand.url`: it keeps the project shape (no CNAME from either the deploy or the production build, and the path the URL carries IS the base path, ahead of the slug derivation), so taking that advice cannot move the site to the domain root and 404 every asset.
80
+ **The direct lane publishes BOTH Pages shapes** ([#361](https://github.com/Omega-JS-Stack/omega/issues/361)): with a `brand.url` naming a domain of the brand's own it writes the CNAME and reports the custom domain; without one it deploys a PROJECT site, the address derived from the repo slug (`https://<owner>.github.io/<name>/`), the build mounted under the matching base path, and the CNAME step skipped entirely (an empty CNAME file would claim a domain). Plan and prefix come from ONE derivation, so the address a deploy reports and the path its build mounts under can never disagree. The repo itself is CONFIG-ONLY ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)): `websiteRepo(config, <target name>)`, where the target name is the target's own folder. Neither `GITHUB_REPOSITORY` nor a git remote is consulted any more, because both name the repo the checkout sits in and publishing there is what #883 retired; a config that names no `repo.org` refuses and says which key to set. A project site still SETS `brand.url`, to its Pages URL, no trailing slash (`https://<owner>.github.io/<name>`), because absolute URLs (canonical, `og:url`, hreflang alternates, the sitemap) build from it, and nothing derives it at build time; the direct lane WARNS on a project-site plan with `brand.url` unset, printing the exact value to set ([#366](https://github.com/Omega-JS-Stack/omega/issues/366)). **A `*.github.io` host is never read as a custom domain**, anywhere that reasons from `brand.url`: it keeps the project shape (no CNAME from either the deploy or the production build, and the path the URL carries IS the base path, ahead of the slug derivation), so taking that advice cannot move the site to the domain root and 404 every asset.
53
81
 
54
- **The web workflow cannot fill the runner's disk** ([#568](https://github.com/Omega-JS-Stack/omega/issues/568), ported from UJM 1.9.33 after somiibo-website failed every deploy for four days on `No space left on device` — the build passed and the gh-pages step took the runner down). `ubuntu-latest` gives about 14 GB, and four levers keep the scaffolded `build.yml` inside it: the checkout is **`fetch-depth: 1`** (the biggest one — a 4.3 GB packed history bought nothing, because NOTHING in an omega build reads git history: the service worker asks `git rev-parse --short HEAD`, which a depth-1 clone answers, `devkit`'s deploy asks `git diff --cached --quiet`, and `--direct` inits a fresh repo in `dist`); the gh-pages step sets **`force_orphan: true`**, so the published branch stays ONE commit instead of growing by a whole site per deploy (it holds build artifacts only — discarding its history is intended, and takes effect on the next deploy); **`df -h /` runs before the build and after it**, the after step with `if: always()` so it still reports when the build itself is what ran out of room; and the file-listing step is **`git ls-files`**, never `ls -R` — the old sweep printed node_modules and truncated the job log exactly when a failure needed reading. Pinned by `packages/web/test/workflow-disk.test.js`; the brand-root twin picks it up on the next omega verb.
82
+ **The web workflow cannot fill the runner's disk** ([#568](https://github.com/Omega-JS-Stack/omega/issues/568), ported from UJM 1.9.33 after somiibo-website failed every deploy for four days on `No space left on device`: the build passed and the gh-pages step took the runner down). `ubuntu-latest` gives about 14 GB, and four levers keep the scaffolded `build.yml` inside it: the checkout is **`fetch-depth: 1`** (the biggest one: a 4.3 GB packed history bought nothing, because NOTHING in an omega build reads git history: the service worker asks `git rev-parse --short HEAD`, which a depth-1 clone answers, `devkit`'s deploy asks `git diff --cached --quiet`, and `--direct` inits a fresh repo in `dist`); the gh-pages push is the framework's own verb, which inits a FRESH repo in `dist` and force-pushes it, so the published branch stays ONE commit instead of growing by a whole site per deploy (it holds build artifacts only; discarding its history is intended; the third-party `peaceiris` action it replaced is gone with [#883](https://github.com/Omega-JS-Stack/omega/issues/883)); **`df -h /` runs before the build and after it**, the after step with `if: always()` so it still reports when the build itself is what ran out of room; and the file-listing step is **`git ls-files`**, never `ls -R`: the old sweep printed node_modules and truncated the job log exactly when a failure needed reading. Pinned by `packages/web/test/workflow-disk.test.js`; the brand-root twin picks it up on the next omega verb.
55
83
 
56
84
  **Cache headers come from the EDGE, not from hosting** ([#751](https://github.com/Omega-JS-Stack/omega/issues/751)). A web deploy publishes to GitHub Pages, and GitHub Pages has no header configuration of any kind — nothing in `packages/web/src` or `packages/manager/src` writes a hosting config for the built site, and the one `firebase.json` a brand carries belongs to the BACKEND target (`packages/backend/templates/firebase.json`), and it declares `hosting.public: dist/public` plus the `omega_api` rewrites for `api.<domain>` — the API host only, and with no `headers` block. So the lifetime of a content-hashed bundle is set by Cloudflare cache rules, which the manager's edge service ships as framework defaults — hashed `/assets` for a year, HTML for a minute ([docs/manager/edge.md](../manager/edge.md)). A brand whose site does not sit behind Cloudflare gets GitHub Pages' own defaults and has nothing to tune.
57
85
 
58
- **The secrets those workflows read come from `.env`** ([#189](https://github.com/Omega-JS-Stack/omega/issues/189)): web's `omega deploy` precheck publishes the target's COMPOSED env — `composeTargetEnv({ targetDir, target: 'web' })`, the brand root's `.env` schema-filtered to the target's keys under an optional target `.env`, files only ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)) — to the brand repo's Actions secrets through `@omega.js/devkit/actions-secrets` (the `gh` CLI, values on stdin, never logged) and regenerates the workflow's env block — since [#627](https://github.com/Omega-JS-Stack/omega/issues/627) both the block and the published set derive from the env schema's `delivery` declarations (`@omega.js/config/env-delivery`), one `KEY: ${{ secrets.KEY }}` line per schema-delivered key, never an `.env` scan. Extension gained the same precheck step ([#680](https://github.com/Omega-JS-Stack/omega/issues/680)), and desktop's `push-secrets` joined them ([#682](https://github.com/Omega-JS-Stack/omega/issues/682)): all three bind the one devkit publisher (`@omega.js/devkit/target-secrets`) with their target string, and desktop adds the only shape difference — a `resolveValue` seam that base64-encodes its file-path signing secrets (`CSC_LINK=config/certs/dev-id.p12`) before the send. No target mints a PAT for this; `gh`'s auth session is the credential. So a CI build has exactly the keys the schema names for it, with no hand-created secrets and no hand-edited workflow. `--no-secrets` opts out; a repo-less/remote-less brand, a CI run, or a checkout that isn't the brand's declared repo skips loudly. **A step that runs only when a secret EXISTS gates on `env`, never on the secret** ([#715](https://github.com/Omega-JS-Stack/omega/issues/715)): the `secrets` context is available in no `if` expression, and one `if: ${{ secrets.X != '' }}` makes GitHub refuse to parse the whole file — a zero-job "workflow file issue" run on every push, however the workflow's own triggers are set. The key goes in the workflow-level `env:` block and the step reads `if: env.X != ''` (web's Cloudflare purge is the live case); consumers heal on the next verb's ensureTarget. Details: [docs/web/index.md](../web/index.md).
86
+ **The secrets those workflows read come from `.env`** ([#189](https://github.com/Omega-JS-Stack/omega/issues/189)): web's `omega deploy` precheck publishes the target's COMPOSED env: `composeTargetEnv({ targetDir, target: 'web' })`, the brand root's `.env` schema-filtered to the target's keys under an optional target `.env`, files only ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)), to the brand repo's Actions secrets through `@omega.js/devkit/actions-secrets` (the `gh` CLI, values on stdin, never logged) and regenerates the workflow's env block, since [#627](https://github.com/Omega-JS-Stack/omega/issues/627) both the block and the published set derive from ONE primitive in `@omega.js/config/env-delivery` (`deliveredKeys(target, modes, { values })`), read off the env schema's `delivery` declarations AND the target's composed PRODUCTION values, one `KEY: ${{ secrets.KEY }}` line per delivered key. Three kinds of key ride it: a schema-NAMED delivery; a `match` FAMILY member the schema knows only as a shape, expanded from the composed env, which is how the OAuth `CONNECTIONS_*` credentials finally reach a dispatched backend deploy ([#876](https://github.com/Omega-JS-Stack/omega/issues/876)); and a CUSTOM key the schema never declared, a consumer's own line in the brand `.env` or its `.env.production`, whose workflow step used to fail silently for want of it ([#835](https://github.com/Omega-JS-Stack/omega/issues/835)). The one SKIP is a key that stays local: a `machineLocal` path, and a declared key whose delivery for this target is the laptop's own `.env` (plus the two names a composed set may never claim: a workflow-owned one, and a `GITHUB_`-prefixed one GitHub refuses as a secret). A custom key takes its target's FILE mode, written into the `.env` the backend runner builds and reaching the runner env alone on web, desktop and the extension. Still never a raw `.env` scan: the composed set is schema-filtered for every key the schema knows. And both halves (what the push publishes, what the backend workflow's `.env` writer names) read that ONE set inside a run, so they cannot drift apart. Extension gained the same precheck step ([#680](https://github.com/Omega-JS-Stack/omega/issues/680)), desktop's joined them ([#682](https://github.com/Omega-JS-Stack/omega/issues/682)), and backend's arrived with its workflow ([#872](https://github.com/Omega-JS-Stack/omega/issues/872): its set adds `OMEGA_SERVICE_ACCOUNT_JSON`, the key file's own bytes, and the workflow writes the target `.env` back out of the block). There is no per-framework BIND any more and no standalone `omega push-secrets` verb ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): ONE function, `publishTargetSecrets({ targetDir, target, logger, dryRun })`, is called by each framework's deploy precheck and by the manage walk's `repo` service (`secrets` op), and the per-target shape differences live in one seam table inside it (desktop base64-encodes a secret that names a FILE and derives its signing paths from the signing tree; backend brings the service-account key as an extra secret; web and the extension bring none). `--dry-run` prints the key NAMES that would publish, the derived lines, and any refusal, and sends nothing ([#895](https://github.com/Omega-JS-Stack/omega/issues/895)); a refusal still throws, because a plan that cannot be made is loud. No target mints a PAT for this; `gh`'s auth session is the credential. So a CI build has exactly the keys the brand delivers to it, with no hand-created secrets and no hand-edited workflow. The step is `fatal` on all four frameworks: a refused or half publish stops the deploy before the dispatch. `--no-secrets` opts out; a repo-less/remote-less brand, a CI run, or a checkout that isn't the brand's declared repo skips loudly. **A step that runs only when a secret EXISTS gates on `env`, never on the secret** ([#715](https://github.com/Omega-JS-Stack/omega/issues/715)): the `secrets` context is available in no `if` expression, and one `if: ${{ secrets.X != '' }}` makes GitHub refuse to parse the whole file: a zero-job "workflow file issue" run on every push, however the workflow's own triggers are set. The key goes in the workflow-level `env:` block and the step reads `if: env.X != ''`; consumers heal on the next verb's ensureTarget. Web's Cloudflare purge no longer needs even that: the gate moved INTO the deploy verb with the purge itself ([#883](https://github.com/Omega-JS-Stack/omega/issues/883)), which reads the env value and skips with a line when it is empty. Details: [docs/web/index.md](../web/index.md).
87
+
88
+ ## The mac signing ladder fails loudly at every rung ([#891](https://github.com/Omega-JS-Stack/omega/issues/891))
89
+
90
+ A green run that publishes an app Gatekeeper refuses is worse than a red one. Proof run one shipped exactly that: four rungs each printed a warning and carried on, and the release published an UNSIGNED, UNNOTARIZED mac app. The rule now, top to bottom: **when the resolved config declares signing for a platform, any rung that cannot produce it is an ERROR that stops the run.** A warning is only for something that still ships (a certificate with under 30 days on it).
91
+
92
+ | Rung | Where | What it does now |
93
+ |---|---|---|
94
+ | 1. certificates walk | `omega manage --service certificates` | A configured type whose `.p12` cannot be produced (no paired private key in either tier, an export failure, an EXPIRED certificate) returns `status: 'error'` naming the type, the expected `csr/{TYPE}/private.key` path, and the fix. It never counts as `synced` ([certificates](../manager/certificates.md)) |
95
+ | 2. `validate-certs` | the `omega deploy` precheck and inside `omega publish`, both STRICT | On macOS, once the config declares `certificates.providers.apple`: an unset `CSC_LINK`/`APPLE_API_KEY` (the signing TREE holds no such file, and the line names both tier paths), a `CSC_LINK` pointing at a missing file, a missing `CSC_KEY_PASSWORD`, an unopenable `.p12`, a missing Keychain identity on a run whose `CSC_LINK` names no certificate (a `CSC_LINK` `.p12` is imported by electron-builder into its own temporary keychain, so the login keychain is read only on the discovery path), or an expired certificate is an ERROR and the deploy stops (the precheck step is `fatal`). It READS the env the boot derived, never a second rule of its own ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)) |
96
+ | 3. the secrets push | the same precheck, `fatal` on ALL FOUR frameworks | A key the env schema marks required for this brand that the `.env` cascade resolves EMPTY stops the deploy listing every one of them and publishes NOTHING: a half signing set on a runner is a green build nobody can install. The mac signing PATHS are never pasted into a `.env` first: `CSC_LINK` and `APPLE_API_KEY` DERIVE from the signing tree at the desktop env load, so the refusal only fires when the tree itself holds no file, and its fix line names both tier paths and `omega manage --service certificates` |
97
+ | 4. the workflow's mac leg | the generated `build.yml` | `No CSC_LINK secret` / `No APPLE_API_KEY secret` `exit 1` with `omega deploy` in the line (its precheck pushes them). There is no per-brand "unsigned mac" switch |
98
+ | 5. the notarize hooks | `afterSign` (the `.app`), `artifactBuildCompleted` (each `.dmg`, awaited before its upload is queued) | Missing credentials or an ad-hoc signature THROW instead of skipping; after notarizing, each artifact is stapled and then PROVED (`xcrun stapler validate`, `spctl --assess`), and anything but acceptance throws, so the release step never runs ([desktop](../desktop/index.md)) |
99
+
100
+ Every rung's message names the command that fixes it, and the two hooks read the tools' own reports rather than trusting an exit code they never checked.
101
+
102
+ ## Publish: assets, stores, and the manual step
103
+
104
+ **The DECLARATION is the switch, on both publishing targets** ([#867](https://github.com/Omega-JS-Stack/omega/issues/867)). `targets.<name>.platforms.<platform>.formats` says what a brand ships, @omega.js/config's format table says what each format IS and needs, and every lane reads that one pair: the manage walk's [publishing](../manager/publishing.md) service to ASK, the publish verbs to SHIP. A publish therefore has exactly three outcomes per format, and the desktop and extension lanes word them identically (both call devkit's `ship-plan`):
105
+
106
+ | Format | What a publish does |
107
+ |---|---|
108
+ | `kind: 'asset'` (dmg, nsis, deb, appimage, the extension zips) | Attached to the release on the brand's ONE public releases repo, under its versionless name. Desktop's electron-builder publish and the extension's `publishToGitHubRelease` both land on `<brand.id>-releases` |
109
+ | `kind: 'store'` with its keys (chrome, firefox, edge, snap) | Published to the store |
110
+ | `kind: 'store'` with a MISSING developer key | REFUSED before anything uploads, naming the key, the declaration that requires it, and `omega manage --service publishing`. Desktop refuses in the publish verb's first step (`ship-keys`), before a minute of build time; the extension refuses per store, after the zips are on the release |
111
+ | `kind: 'store'` with the keys but no LISTING id | The zip stays on the release and ONE manual step prints: create the listing at the store's console, upload that zip, set `targets.<name>.listings.<browser>.id` in `config/omega.json5`, re-run. Nothing fails: no API can create a listing, so this is a pending human step, not a broken publish |
112
+
113
+ **A credential is owed only where the brand's own config makes it due.** The env schema gates each one (`requiredWhen`), so a desktop brand that never mentions the snap is never asked for `SNAPCRAFT_STORE_CREDENTIALS` and never refused for it, while one that writes `platforms.linux.formats.snap` owes the login. The Windows signing set narrows the same way, to the strategy the brand configured. A BUILD keeps its clean skip either way (`build-config` drops the snap target when the login is absent): a build puts nothing in front of users.
114
+
115
+ **Firefox needs no listing id at all**: its add-on id IS the manifest's gecko id, so the publish that CREATES the listing writes the id back into the brand config itself ([#893](https://github.com/Omega-JS-Stack/omega/issues/893)). Chrome and Edge assign theirs in a dashboard, which is why those two are the ones the walk asks for and the ones a publish can leave a manual step behind for.
59
116
 
60
117
  ## The verb — `omega deploy` on every target
61
118
 
62
- | Target | Behavior | Flags |
63
- |--------|----------|-------|
64
- | web | linked-local auto-detect → DIRECT lane; else sync (plain git commit + push — publishes nothing) → dispatch `build.yml` | `--dry-run` (print the exact plan, send nothing), `--local` (build only), `--no-sync`, `--direct` (force the direct lane: build + push dist to gh-pages, then Cloudflare purge) |
65
- | extension | linked-local auto-detect → `npm run release` (local build + store publish); else sync → dispatch `publish.yml` | `--dry-run`, `--no-sync` |
66
- | desktop | linked-local auto-detect → `npm run release:local` (local build + sign + publish); else `--dry-run` prints the dispatch / delegates to `omega release` (dispatch + live CI log streaming) | `--dry-run`, `--platforms` |
67
- | backend | artifact cleanup-policy pre-step → local-package staging (ALWAYS auto — file: deps pack into the artifact) → `firebase deploy` → public-invoker IAM fix | `--only <targets>` pass-through (e.g. `--only hosting` deploys on Spark where functions would demand Blaze) |
68
- | backend, `projectType: 'custom'` | REFUSED — a custom-server backend has no Cloud Functions to publish ([#584](https://github.com/Omega-JS-Stack/omega/issues/584)). The container host is the publisher; see the Render lane below | — |
119
+ Every target runs the SAME three beats ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): the secrets precheck, the lane (`resolveDeployLane`: snapshot or push, below), then the dispatch. `--direct` on any of them runs the deploy from THIS machine instead, and it is checked BEFORE the precheck on all four: a local deploy publishes nothing to the repo and needs no `gh` session, so it never pushes the target's secrets (desktop's signing certs, the extension's store credentials) on the way past.
120
+
121
+ **Desktop and extension CI run the publish task DIRECTLY** (`npm run release:local`, `npm run build` under the publish flag) rather than the `deploy --direct` verb web and backend CI run: the verb also runs the consumer `hooks/deploy/pre.js`, a pre-deploy step that belongs to the developer's machine (the playground's prune must not run on three CI legs at once), plus the drift check the snapshot already passed locally. By design, not a gap ([#865](https://github.com/Omega-JS-Stack/omega/issues/865)). Both pairs are PINNED to each other by a build test that reads the command out of the scaffolded workflow itself (the extension's `publish.yml` build step, desktop's mac and linux `npm run release:local` steps), expands it through the target's own scripts and runs the verb against a stubbed publish: a laptop lane and its runner lane cannot drift apart silently, whichever of the three moves.
122
+
123
+ **One shape for the two lines a dispatch prints**, on every target and through each one's plain log level: the dry-run header `DRY RUN (<mode> lane, ref <ref>), would send:` followed by the plan, and on a real send `Dispatched <workflow> (<mode> lane, ref <ref>): <what CI does>.` (desktop prints it from `omega release`, which owns its dispatch). On the snapshot lane the label carries the sha the run dispatches against, whoever pushed it: `Dispatched <workflow> (snapshot lane, ref main @ <sha7>)`.
124
+
125
+ **Every dispatched deploy then FOLLOWS its run** ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)), through the ONE follower in devkit (`@omega.js/devkit/deploy-follow`, ported out of desktop's `omega release`, which was the only target that had one): the run this dispatch started is found by its timestamp, the run and its jobs are polled, and each job's log is streamed with its name in front as the steps that produced it close. On the snapshot lane that run's `head_sha` must equal the sha this deploy pushed, or the verb fails naming both shas and the run URL ([#902](https://github.com/Omega-JS-Stack/omega/issues/902)): a run building a tree this deploy did not push is not this deploy's verdict, whatever color it goes. The status banner prints on a TRANSITION only, a job GitHub marked skipped renders `⊘` rather than a cross (a run that ships two of three platforms skips the legs its `platforms` input left out), and a poll failure is retried up to twelve ticks rather than turned into a verdict: the runner keeps building whether or not this laptop reaches api.github.com for one tick. **The verb's exit code is the run's conclusion**, so a red run fails the deploy that started it instead of reporting success at the dispatch. Everything the verb prints, from the scaffold and the precheck's refusals to the followed run, is teed to `<targetRoot>/logs/deploy.log` ([logging.md](logging.md)); tees stack, so a brand fan-out's own `logs/deploy.log` gets the same lines.
126
+
127
+ | Target | Dispatch lane | `--direct` | Flags |
128
+ |--------|---------------|------------|-------|
129
+ | web | `build.yml` | build + force-push dist to the website repo's `gh-pages`, point Pages at that branch and domain (devkit `ensurePages`), then the Cloudflare purge | `--dry-run` (print the exact plan, send nothing), `--local` (build only), `--direct` |
130
+ | extension | `publish.yml` | `npm run build` with `OMEGA_IS_PUBLISH=true`, the ONE command CI runs ([#865](https://github.com/Omega-JS-Stack/omega/issues/865)): the build series ENDS in the publish task, so the old `npm run release` (`build && publish`) entered it twice and published each version to every store twice over | `--dry-run`, `--direct` |
131
+ | desktop | `build.yml` (`omega release` is the same dispatch with live CI log streaming) | `omega publish --local` for the HOST platform; a `--platforms` value the host cannot build errors, because a cross-platform build is CI-only | `--dry-run`, `--platforms` (`mac` \| `windows` \| `linux` \| `all`, the one vocabulary, [#867](https://github.com/Omega-JS-Stack/omega/issues/867)), `--direct` |
132
+ | backend | `deploy.yml` | the Firebase deploy from this machine: artifact cleanup-policy pre-step, the pack step, `firebase deploy`, public-invoker IAM fix (this is what the runner itself runs) | `--dry-run`, `--direct`, `--only <targets>` pass-through (e.g. `--only hosting` deploys on Spark where functions would demand Blaze) |
133
+ | backend, `projectType: 'custom'` | REFUSED: a custom-server backend has no Cloud Functions to publish ([#584](https://github.com/Omega-JS-Stack/omega/issues/584)). The container host is the publisher; see the Render lane below | REFUSED | — |
134
+
135
+ **A brand whose cloud project is SHARED never deploys its backend** ([#882](https://github.com/Omega-JS-Stack/omega/issues/882)): `cloud.shared: true` (`config/omega.json5` → `cloud`) makes the backend verb quit at its first gate, ahead of the scaffold, the precheck and the version assert, printing ONE line (`Skipping the backend deploy: cloud.shared is true (config/omega.json5 → cloud), and a shared project is never deployed from a brand`) and exiting 0. Every lane takes it, because the PROJECT is the reason: a laptop `omega deploy`, the brand-root fan-out (which reads the exit 0, moves on to the group and ticks the target in its summary), and the composed workflow's own `omega deploy --direct` step, which makes a dispatch of that workflow a green no-op. Nothing changes about the workflow itself: it still composes for the target like every other one, so the skip lives in exactly one place instead of two that could disagree. Why the verb rather than a per-target switch: a shared project belongs to several brands at once, the manage cycle already limits itself to the per-brand operations there ([the cloud service](../manager/cloud.md)), and a deploy would publish this brand's functions and rules over the project every one of those brands runs on. It is a SKIP, not a refusal: the deploy lane refuses (exit 1) only what would ship broken, such as version drift or an empty required secret.
69
136
 
70
137
  Every one of them runs the OMEGA license check once on the way — the production build for web/desktop/extension, the deploy itself for backend — and bakes the verdict into that artifact. What each target does with it, and what a keyless verdict costs: [publishing.md § The license check](publishing.md#the-license-check-320).
71
138
 
72
- ### The brand-root fan-out — `omega deploy` at a brand root (cp251)
139
+ **Desktop and extension run the target's `hooks/deploy/pre.js` first** ([#899](https://github.com/Omega-JS-Stack/omega/issues/899), [#900](https://github.com/Omega-JS-Stack/omega/issues/900)): after the local scaffold and before the precheck, on both lanes, through the same consumer hook runner that surface's build hooks use (`packages/desktop/docs/hooks.md`, and the package task's loader on extension); a dry run skips it, because a hook may act on the world. The playground's hook now ONLY prunes, one shared helper (`brands/playground-omega/scripts/release-lane.js`) both target hooks call with their tag family (`v<version>` for desktop, `extension-v<version>` for extension): it deletes every release of that family on `Omega-JS-Stack/playground-releases` except the newest, so two releases stay live per target. The VERSION comes from `omega bump` at the brand root ([#869](https://github.com/Omega-JS-Stack/omega/issues/869)), which writes the brand's one number into the root and every target, and every framework's `omega deploy` refuses a target whose version differs from the root's, before its precheck. The per-target bump the lane used to do is gone with it. Every deploy still publishes a version nothing has seen, which is the point: AMO refuses a version it already holds ("Version 0.0.1 already exists"), and a republish of a live version is not a publish at all. A fresh version also never trips electron-publish's two-hour rule, which is what #899's delete-and-redeploy was working around. [#192](https://github.com/Omega-JS-Stack/omega/issues/192) migrates the runner and those hooks into the universal hook system.
140
+
141
+ ### The brand-root fan-out: `omega deploy` at a brand root (cp251)
73
142
 
74
- At a brand root the dispatcher hands `omega deploy` to `@omega.js/manager`'s deploy command (`src/commands/deploy.js`), which fans out over the brand's targets — **not a new executor**: each target's own framework `omega deploy` verb runs (the table above), spawned sequentially with streamed output, cwd = the target dir, under the target's own Node.
143
+ At a brand root the dispatcher hands `omega deploy` to `@omega.js/manager`'s deploy command (`src/commands/deploy.js`), which fans out over the brand's targets, **not a new executor**: each target's own framework `omega deploy` verb runs (the table above), cwd = the target dir, under the target's own Node.
75
144
 
76
- - **The DELIVERY lane runs first, once** ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)): before any target is spawned, the fan-out walks the same `BOOT_SERVICES` lane an `omega dev` boot walks (asked for by NAME, so the list can only be that one constant), so config, assets and certs reach the targets before anything publishes them. Errors there stop the run and nothing deploys; `--dry-run` reaches the lane too.
77
- - **Order: backend first, then web, then extension/desktop** — the API must be live before the surfaces that point at it.
78
- - **The target picker (consumed here, never forwarded)**: `--target=<target|targetDir>[,…]` names the exact set, the same picker `omega test` takes ([#780](https://github.com/Omega-JS-Stack/omega/issues/780)). A token matching nothing is an error, never a deploy-everything fallback. `--only`/`--except` are retired: passing either fails, naming `--target=`.
79
- - **Every other flag forwards verbatim** to each target's deploy (`--dry-run`, `--direct`, `--no-sync`, `--platforms`, …). Backend-target `--only` values (`--only hosting`) are firebase's own flag, target-level — run those from `targets/backend`.
80
- - **A failing target stops the run** (later targets depend on it); `--continue-on-error` overrides. Any failure → exit 1.
145
+ - **The DELIVERY lane runs first, once** ([#678](https://github.com/Omega-JS-Stack/omega/issues/678)): before any target is spawned, the fan-out walks the same `BOOT_SERVICES` lane an `omega dev` boot walks (asked for by NAME, so the list can only be that one constant), so config and assets reach the targets before anything publishes them (signing material is read in place, so nothing delivers it). Errors there stop the run and nothing deploys; `--dry-run` reaches the lane too.
146
+ - **Order: BACKEND alone, then web, desktop, extension and any custom target CONCURRENTLY** ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): the API must be live before the surfaces that point at it, and those surfaces depend on nothing of each other's, so they publish together. A `--target=` pick keeps the same shape over the picked set.
147
+ - **Every selected target is SCAFFOLDED first** ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): after the delivery lane and before the push, the fan-out calls each selected target's framework `ensureTarget` IN-PROCESS, sequentially, in the same order. Every framework exposes that function at one subpath, `@omega.js/<framework>/ensure-target` (an exports entry on web, desktop and extension; a package-root file on the backend, which ships no exports map), and the manager's `resolveTargetScaffold` is the one place that resolves it (a module that fails to resolve OR throws on the way in is one named error, never a raw stack). One contract on all four: `async ({ projectDir, log, warn }) => { written, merged, changed }`, awaited by the caller (so a sync implementation like the backend's is legal), every warning-shaped line routed through `warn` and nothing printed directly. There is no verb and no flag for it: a deploy always scaffolds first. A target's scaffold is what COMPOSES that target's workflow into the brand root, and the snapshot below is what carries it to the runner: scaffolding only inside each target's own deploy verb put it AFTER the push, so a workflow re-rendered this run did not ride this run's snapshot, and a target's first deploy found no workflow on the mirror at all. A scaffold that throws stops the run there, before anything is pushed. A `--dry-run` scaffolds too (every verb does), and the target verbs scaffold again on their way past, idempotently. The composed files that scaffold writes are also what the delivery compares against the default branch ([#915](https://github.com/Omega-JS-Stack/omega/issues/915)), so a workflow re-rendered this run reaches GitHub in the same run. A custom target has no framework scaffold and is skipped loudly, which never blocks the push.
148
+ - **The lane's DELIVERY runs once per run** ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): the root resolves the lane once and runs the ONE delivery helper (`deliverLane`, the same one every target verb runs for itself) before it spawns a single target, because the whole group shares one brand folder and one `.git`. It refuses a checkout behind the default branch, pushes the composed workflow files there when they differ, packs the linked frameworks and force-pushes the brand folder to `omega-deploy` ONCE, prints `Snapshot pushed: <owner>/<repo> <ref> @ <sha7>`, and hands every target verb `--snapshot=<sha>` so each dispatches against that very commit instead of overwriting it under its siblings. Nobody types that flag: it is the root's word to the verbs (a verb run alone in a target dir still performs its own delivery, and `--snapshot` on a brand with no repo is an error, since nothing was pushed to skip). A `--dry-run` and a `--direct` run deliver nothing at all, and forward no sha. The delivery is the run's FIRST act, ahead of every target's precheck, so a refusal later leaves the deploy branch carrying this run's tree with nothing dispatched against it.
149
+ - **The group's output is prefixed** ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): each concurrent child's stdout and stderr are piped and re-emitted line by line behind `[<target>] `, so the brand's `logs/deploy.log` carries the interleaved run readably while each target's own `logs/deploy.log` ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)) keeps its clean copy. A group child gets NO stdin: a step that would prompt refuses instead of stalling three deploys behind it. The backend, alone ahead of the group, keeps the inherited terminal.
150
+ - **The target picker (consumed here, never forwarded)**: `--target=<name>[,…]` names the exact set (a target's NAME is its folder, [#886](https://github.com/Omega-JS-Stack/omega/issues/886)), the same picker `omega test` takes ([#780](https://github.com/Omega-JS-Stack/omega/issues/780)). A token matching nothing is an error, never a deploy-everything fallback. `--only`/`--except` are retired: passing either fails, naming `--target=`.
151
+ - **Every other flag forwards verbatim** to each target's deploy (`--dry-run`, `--direct`, `--platforms`, …). A `--dry-run` RUNS each target's precheck ([#895](https://github.com/Omega-JS-Stack/omega/issues/895)): every step is a read or a plan under it, so the preview is the real one, refusals included. Backend-target `--only` values (`--only hosting`) are firebase's own flag, target-level: run those from `targets/backend`.
152
+ - **A failing BACKEND stops the run before the group** (the surfaces point at its API); `--continue-on-error` overrides, and it governs that gate alone. Inside the group every target runs to completion, and the run then exits 1 naming each one that failed. Any failure → exit 1. A dispatched target is not done when its dispatch is accepted: it FOLLOWS its run to the conclusion ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)), so the summary's `✓ <target>` means the run went green, and a red run stops the fan-out where it used to print a tick beside it.
153
+ - **The version check is each target verb's, inherited here** ([#869](https://github.com/Omega-JS-Stack/omega/issues/869)): the fan-out runs no drift check of its own. Every framework's `omega deploy` refuses a target whose `package.json` version differs from the brand root's, naming the target and `omega bump`, so a fan-out over a drifted brand stops at the first target that drifted.
81
154
  - **Deliberate stays deliberate**: nothing invokes the brand-root verb automatically — it is only the human-typed command (scaffolded as the brand root's `deploy` npm script; the workspace service's `scripts` op guarantees the script exists — it mints a missing `deploy: "omega deploy"` and never rewrites script values).
82
155
 
83
156
  #### The container lane — a custom-server backend ([#584](https://github.com/Omega-JS-Stack/omega/issues/584))
@@ -90,7 +163,7 @@ A backend declaring `targets.backend.projectType: 'custom'` runs its Express app
90
163
  - **Order is unchanged** — the backend still deploys first, container or not; the surfaces that call the API go after it.
91
164
  - The other half is a trap worth knowing: the scaffolded backend `deploy` script is `npx omega deploy`, which in custom mode ends in the framework's refusal. That is deliberate — it fails loudly, on the line that says what to put there, instead of quietly deploying nothing.
92
165
 
93
- **Mirrored rule (Ian 2026-07-20): every deploy verb auto-detects linked local packages (`findLocalSpecs` — tree-wide, cp194) and takes its LOCAL-ARTIFACT lane**, loudly, so a linked brand ships the local framework without flags or errors; CI dispatch is only for registry-clean trees (`omega i live` restores them). Dry-runs show whichever plan would actually run. Sync is plain git via the shared `syncWorkingTree` (the old `npu sync` shell-out is gone — npu is a personal tool consumer machines don't have).
166
+ **A linked tree no longer picks its own lane** ([#872](https://github.com/Omega-JS-Stack/omega/issues/872), retiring the 2026-07-20 auto-detect rule): a `file:` @omega.js spec used to flip web, extension and desktop into their local-artifact lane behind the user's back, which meant four different answers to one question. Now `findLocalSpecs` feeds `resolveDeployLane` instead, a linked tree PACKS its frameworks into the one lane's snapshot (the tarballs travel, the runner still builds), and running the deploy on this machine is the explicit `--direct`. Dry-runs show the plan that would actually run. Since [#915](https://github.com/Omega-JS-Stack/omega/issues/915) `linked` no longer picks a lane at all: it picks whether the pack step runs.
94
167
 
95
168
  ## One command, repo to live — manage + deploy + verify ([#48](https://github.com/Omega-JS-Stack/omega/issues/48))
96
169
 
@@ -117,56 +190,53 @@ transport is called, so the offline brands stay offline. Home: `packages/manager
117
190
  (both transports — fetch and DNS — are injected seams, so the tests run
118
191
  fully offline).
119
192
 
120
- ## Local frameworks in production artifacts (iterate without publishing)
193
+ ## Local frameworks in production artifacts: the ONE pack step ([#872](https://github.com/Omega-JS-Stack/omega/issues/872))
121
194
 
122
- A brand linked to the local monorepo (`file:` specs) can ship the LOCAL framework code to production — no npm publish involved. This is a supported feature (Ian 2026-07-20: iterate fast on unproven framework changes without burning registry versions). Per target:
195
+ A brand linked to the local monorepo (`file:` specs) ships the LOCAL framework code to production with no npm publish involved. Supported on purpose (Ian 2026-07-20: iterate fast on unproven framework changes without burning registry versions), and since [#872](https://github.com/Omega-JS-Stack/omega/issues/872) it is ONE mechanism instead of four: the tarballs travel with the snapshot and the RUNNER does a plain `npm install`. No clone, no `preinstall`, nothing framework-aware on the box.
123
196
 
124
- | Target | Lane | How the local framework reaches production |
125
- |---|---|---|
126
- | backend | `omega deploy` (always direct) | `stage-local-packages`: every `file:` @omega.js dep — and, transitively, every @omega.js runtime dep of those that resolves through a node_modules SYMLINK — is `npm pack`ed — the package's REAL prepare runs, so vendoring makes the tarball self-contained — into `functions/omega_modules/*.tgz`; the staged manifest respells to the tarball and the lockfile regenerates, so Cloud Build installs the LOCAL framework verbatim; everything restores after the upload |
127
- | web | `omega deploy --direct` | the site builds HERE with the linked framework; only built output pushes to gh-pages (the CNAME + purge ride along) |
128
- | desktop | `npm run package` / `npm run release:local` | the build bundles the linked framework into the artifact; `release:local` signs + publishes that locally-built artifact |
129
- | extension | `npm run build` → `packaged/<browser>/` | same — the bundles carry the linked framework; upload the zip (or `OMEGA_IS_PUBLISH=true npm run build`) |
130
-
131
- The ONE lane that inherently needs the registry is CI-dispatch builds (web `omega deploy` without `--direct`; extension/desktop CI publish workflows): CI rebuilds from pushed source and cannot install `file:` specs — exactly what the tree-wide guard refuses, pointing back at the lanes above. cp241 made every packed tarball self-contained (vendoring in all six publishables), so the backend staging lane works for ANY linked @omega.js dep.
132
-
133
- **Every successful deploy records itself (cp196)**: `<target>` under the `deploy` section of the brand's `.omega/state.json` via `@omega.js/devkit/deploy-record` (`recordDeploy`/`readDeployRecord` — brand-root-resolved from any target dir; gitignored, per-machine). state.json is the ONE per-machine record file, sectioned per fact kind ([#479](https://github.com/Omega-JS-Stack/omega/issues/479)) — future record-shaped facts join it as sibling top-level sections, and deploy-record passes every section but its own through verbatim (caches, locks, certs and logs are not records and keep their own homes). #434 retired the file's old CONTENT, not the file: a brand that has not run the state-retirement migration yet keeps its config-shaped keys sitting beside the records, untouched, and its records are read in place. The one adoption left is the interim `.omega/deploys.json` the records spent 0.45.0 in ([#449](https://github.com/Omega-JS-Stack/omega/issues/449)): a brand still carrying that file has it folded into state.json on the first read or write — one-time and LOUD, ONE line naming what moved and that the old file was removed. **Multi-instance targets deploy per target, and records key per target**: each target's `omega deploy` ships ITS instance (`omega deploy` in `targets/website-admin` publishes the admin site) — the primary keeps the bare `<target>` key (adopted records stay valid) and any other instance records under `<target>:<id>` (`web:admin`), so the testing service's never-deployed/deployed-but-down split works per instance. The manager's testing service reads it to split **never deployed** (live-URL checks warn with a "run `omega deploy` when ready" nudge) from **deployed but down** (an honest error), and ADOPTS a record when a record-less brand's live URL answers — so fresh clones of long-deployed brands self-heal on their first manage run. Dry-runs never record.
134
-
135
- ### Backend: local-package staging
136
-
137
- Cloud Build only installs what's inside the uploaded functions folder, and its
138
- buildpack runs `npm ci` (lockfile required) — so a local-first dep like
139
- `"@omega.js/backend": "file:../../../../../packages/backend"` can never deploy
140
- as-is. When `--only` includes functions, the deploy command stages the
141
- functions dir in place (`src/cli/utils/stage-local-packages.js`): each
142
- outside-the-folder `file:` dep is `npm pack`ed into `functions/omega_modules/*.tgz`
143
- (pack runs the package's prepare, so the tarball reflects current source),
144
- package.json is respelled to the tarball, and the lockfile is regenerated for
145
- the staged shape. After the deploy — success or failure — the original
146
- package.json + package-lock.json are restored verbatim and `omega_modules/` is
147
- removed. Published (registry) deps are untouched; a functions dir with no
148
- outside `file:` deps stages nothing. The Artifact Registry cleanup policy is
149
- ensured before deploying because firebase-tools otherwise exits 1 AFTER a
150
- successful functions deploy, which would skip the public-invoker fix.
151
-
152
- **"Local" is transitive ([#331](https://github.com/Omega-JS-Stack/omega/issues/331)).** A packed framework still declares its own
153
- dependencies by registry spec, and `@omega.js/client` is a real runtime
154
- dependency of `@omega.js/backend` that is never vendored — unpublished under
155
- the publish latch, so regenerating the lock 404'd on it and no deploy ran. The
156
- lane therefore packs the CLOSURE: after each target, its `@omega.js` runtime
157
- deps that resolve through a node_modules **symlink** (the local-era shape npm
158
- workspaces and `mgr i local` produce — the registry may have no copy of what
159
- the link points at) are packed too, and the staged manifest gets an npm
160
- `overrides` entry per packed transitive dep, since only an override redirects a
161
- NESTED requirement to the artifact beside it. A dep resolving to a real
162
- directory is a registry install and is left to Cloud Build. Result: lock
163
- regeneration never asks the registry for a local package.
164
-
165
- **A failing stage is loud and clean.** Any failure inside the lane restores the
166
- functions folder first (manifest, lockfile, `omega_modules/` — no half-staged
167
- tree) and then throws with the lane named, so the CLI's error path prints it
168
- and exits nonzero: nothing deploys, and the next run starts from the original
169
- shape.
197
+ ### The lane matrix
198
+
199
+ ONE lane since [#915](https://github.com/Omega-JS-Stack/omega/issues/915), and
200
+ the two refusals it still owes:
201
+
202
+ | The brand tree | Mode | Ref | Why |
203
+ |---|---|---|---|
204
+ | any brand with a git repo (nested or its own toplevel, linked or registry clean) | `snapshot` | `omega-deploy` | the deploy branch IS what CI builds, for every brand and every starter of a deploy, so the brand's real history never carries packed tarballs and nobody's uncommitted tree is committed to reach a runner. `nested` and `linked` stay on the lane as FACTS (the behind check skips a nested brand, the pack step runs for a linked one, the secrets publisher reads `nested`), never as a second lane |
205
+ | **linked** with no git repo at all | REFUSED | | a snapshot is built out of a git index, so the lane says so by name (`git init` the brand, or `omega i live` for registry versions) instead of dying later on a raw `fatal: not a git repository` |
206
+ | plain, no git repo at all | `dispatch` | `omega-deploy` | nothing to snapshot FROM and nothing linked that would need to (the lane carries `repo: false`), so the lane is the wait and the dispatch alone, on whatever the branch already holds |
207
+
208
+ ### The pack step
209
+
210
+ `@omega.js/devkit/pack-local`'s `stageLocalPackages({ dir })`, lifted out of the backend (its former `src/cli/utils/stage-local-packages.js`) so all four targets share one implementation. `dir` is an INSTALL ROOT: a brand root with workspaces, a standalone target, or a backend `functions/` folder.
211
+
212
+ - **What it packs**: every `file:` dep pointing OUTSIDE `dir`, read from `dir/package.json` AND from every workspace member manifest; plus, transitively, every `@omega.js` runtime dep of a packed package that resolves through a node_modules SYMLINK (the local-era shape workspaces and `omega i local` produce, which the registry may have no copy of).
213
+ - **Where the tarballs land**: `dir/omega_modules/*.tgz`, one `npm pack` each. Pack runs the package's REAL prepare, so the tarball reflects current source and is self-contained (cp241 vendored every publishable, which is what makes this work for any linked @omega.js dep).
214
+ - **How the manifests are respelled**: each manifest's own direct spec becomes a path relative to THAT manifest (`file:../../omega_modules/x.tgz` from `targets/web`); transitive ones become npm `overrides` in the ROOT manifest, since only an override redirects a NESTED requirement to the artifact beside it. Then `dir/package-lock.json` is deleted and regenerated (`npm install --package-lock-only --ignore-scripts --no-audit --no-fund`), so lock regeneration never asks the registry for an unpublished package.
215
+ - **The restore**: the call returns `{ staged, restore }`, and `restore` rewrites every touched manifest and the lockfile byte-for-byte and removes `omega_modules/`. Any failure inside the lane restores FIRST and then throws with the lane named, so nothing deploys off a half-staged tree.
216
+ - **Idempotent across hops**: a `file:` spec that already points at a `.tgz` (a dep or an override, the manifest's own or inherited from the workspace root) is COPIED into this dir's `omega_modules/` and respelled, never packed again. That second hop is how the runner's backend `omega deploy --direct` stages its `functions/` folder out of the snapshot's tarballs, and how a local `@omega.js/client` override reaches Cloud Build.
217
+
218
+ Cloud Build is why the backend needed this first: the buildpack installs only what sits inside the uploaded functions folder, with `npm ci` against a lockfile, so a `file:` spec pointing anywhere outside it can never deploy as-is. The Artifact Registry cleanup policy is still ensured before deploying, because firebase-tools otherwise exits 1 AFTER a successful functions deploy, which would skip the public-invoker fix.
219
+
220
+ ### The snapshot push
221
+
222
+ `deployViaDispatch` runs the lane in this order: read the repo's DEFAULT branch once and heal it when published output has taken it over (a `gh-pages` default moves back to `main` here, before any workflow file is written: [#922](https://github.com/Omega-JS-Stack/omega/issues/922), once per repo, and never on a dry run, which returns before the delivery reads anything at all), refuse a checkout behind it (skipped for a nested brand, whose git toplevel is somebody else's repo), push the composed workflow files to that branch when they differ (`pushWorkflowFiles`), pack (when the tree is linked), push the folder to `omega-deploy` (`@omega.js/devkit/deploy-snapshot`'s `pushSnapshot`), restore, wait for the REF to carry the pushed sha (`waitForRef`, [#902](https://github.com/Omega-JS-Stack/omega/issues/902)), wait for the workflow (`waitForWorkflow`), dispatch.
223
+
224
+ **A `snapshot` sha skips everything up to the ref wait** ([#901](https://github.com/Omega-JS-Stack/omega/issues/901)): it says the caller already put this run's snapshot on the lane's ref, so the verb keeps the wait and the dispatch and its dispatch line names the sha (`Dispatched <workflow> (snapshot lane, ref omega-deploy @ <sha7>)`). The brand-root fan-out is its ONE caller, through `--snapshot=<sha>` on all four verbs; on a brand with no repo it is an error, because nothing was pushed to skip.
225
+
226
+ **The workflow-file push, in detail** ([#915](https://github.com/Omega-JS-Stack/omega/issues/915)): every composed `<brandRoot>/.github/workflows/*.yml` is byte-compared with the copy the default branch holds (contents API per file, a 404 being "missing"). When every one matches, the branch is not touched and ONE line says so. Otherwise exactly the missing and changed files are written through the git DATA api: a blob each, a tree with `base_tree` set to the head's tree, a commit parented on the head, then `refs/heads/<default>` updated; an EMPTY repo gets a parentless commit and a created ref. The message is `chore(ci): compose <names>`. No working tree, index or local branch takes part on either side, and a `--dry-run` prints the file names and the branch without reading anything at all.
227
+
228
+ - **What rides along**: the brand FOLDER as it stands, which is every tracked file plus every untracked-not-ignored one, so the composed workflows, the staged `omega_modules/` tarballs and the regenerated lockfile all reach the runner. What the brand's own ignore rules exclude stays home (`node_modules`, `.omega`, build output), and a tracked-but-ignored file stays in because the temporary index is seeded from HEAD first.
229
+ - **A force push out of a TEMPORARY git index** (`GIT_INDEX_FILE`): no stash, no checkout, no local branch, and the working tree, the real index and every local branch are untouched. The commit is PARENTLESS and pushed by sha, so a mirror carries no history: two snapshots are siblings, never a fast-forward, and the private tree a nested brand was cut from never reaches its public repo. The pack step's restore runs afterwards ALWAYS, failure included.
230
+ - **The guard, before any of it**: the push REFUSES when the brand's own ignore rules would hide `omega_modules/` or the regenerated `package-lock.json` (`git check-ignore` per path). A snapshot without them carries `file:` specs pointing at tarballs that never travelled, so the runner installs nothing, and the refusal names the hidden path, the `git check-ignore -v <path>` that finds the rule, and the `!omega_modules` line that un-ignores it.
231
+ - **Then the wait**: **GitHub registers a workflow from the repo's DEFAULT branch**, not from the ref a dispatch targets. Both `GET /repos/{o}/{r}/actions/workflows/{file}` and the `workflow_dispatch` that follows read the file from the default branch, and the `ref` only picks which checkout the run uses. That is the whole reason `pushWorkflowFiles` exists, and it is why the wait still polls the listing: GitHub indexes a freshly pushed file a few seconds after it arrives, so the step waits for it and fails loudly past the budget instead of asking a human for a second attempt. A brand's FIRST deploy is the run that puts the composed file there.
232
+
233
+ ### A self-hosted-runner workflow fires only on dispatch
234
+
235
+ Standing rule: a workflow with a job on a self-hosted runner carries the one dispatch trigger (`workflow_dispatch`) and nothing else ([#880](https://github.com/Omega-JS-Stack/omega/issues/880), narrowed to the single event by [#923](https://github.com/Omega-JS-Stack/omega/issues/923): `repository_dispatch` names an event TYPE and never a ref, so such a run checks out the DEFAULT branch, which never carries the deploy tree the snapshot push put on the deploy ref, and nothing in OMEGA ever sent one). A push trigger on such a file queues jobs on a box that may be off, and on a public repo it hands anyone who can open a PR a shell on hardware in the house. Desktop's `build.yml` (the Windows EV-token signer) is the live case; the guard that keeps a template from growing a second trigger is [#875](https://github.com/Omega-JS-Stack/omega/issues/875).
236
+
237
+ **The rule is enforced in three places** ([#875](https://github.com/Omega-JS-Stack/omega/issues/875)), because the shape of a workflow file is one careless edit from undoing itself. (1) The desktop template's `windows-sign` job carries `&& github.event_name == 'workflow_dispatch'` in its `if:`, so a `push` or `pull_request` trigger added to that file later SKIPS the sign job instead of signing a stray build: a skip, never a failure, and `finalize`'s `always()` keeps the unsigned-upload path working through it. (2) The BOX refuses on its own, before a job's first step: `omega runner install` writes a job-started hook beside the runner and points each registration's `ACTIONS_RUNNER_HOOK_JOB_STARTED` at it, and the hook exits nonzero unless the event is a `workflow_dispatch`, the repository is on the box's `allowed-repos.txt` and the actor is on its `allowed-actors.txt` ([packages/desktop/docs/runner.md](../../packages/desktop/docs/runner.md)). The allow lists live ON THE BOX, never in the shared template: a runner is registered to one owner and the box owner configures it (Ian 2026-09-10), and the dispatch identity is the deploy token's user, the same account (Ian 2026-09-13). (3) The manage walk's `runners` ensure reads the brand's composed workflows and warns, one line per file and trigger, on a `push` or `pull_request` in any workflow that puts a job on a self-hosted label.
238
+
239
+ **Every successful deploy records itself (cp196)**: `<target>` under the `deploy` section of the brand's `.omega/state.json` via `@omega.js/devkit/deploy-record` (`recordDeploy`/`readDeployRecord`: brand-root-resolved from any target dir; gitignored, per-machine). state.json is the ONE per-machine record file, sectioned per fact kind ([#479](https://github.com/Omega-JS-Stack/omega/issues/479)), future record-shaped facts join it as sibling top-level sections, and deploy-record passes every section but its own through verbatim (caches, locks, certs and logs are not records and keep their own homes). #434 retired the file's old CONTENT, not the file: a brand that has not run the state-retirement migration yet keeps its config-shaped keys sitting beside the records, untouched, and its records are read in place. The one adoption left is the interim `.omega/deploys.json` the records spent 0.45.0 in ([#449](https://github.com/Omega-JS-Stack/omega/issues/449)): a brand still carrying that file has it folded into state.json on the first read or write: one-time and LOUD, ONE line naming what moved and that the old file was removed. **Every target deploys itself, and records key by target NAME** ([#886](https://github.com/Omega-JS-Stack/omega/issues/886)): each target's `omega deploy` ships that target (`omega deploy` in `targets/admin` publishes the admin site) and stamps its own name (`admin`), so a brand running several web targets keeps one record per target and the testing service's never-deployed/deployed-but-down split works per target. The manager's testing service reads it to split **never deployed** (live-URL checks warn with a "run `omega deploy` when ready" nudge) from **deployed but down** (an honest error), and ADOPTS a record when a record-less brand's live URL answers, so fresh clones of long-deployed brands self-heal on their first manage run. Dry-runs never record.
170
240
 
171
241
  ### Backend: resolved-config staging (#31)
172
242
 
@@ -182,15 +252,47 @@ map, so the deployed runtime's own `defaults ← shared ← targets[backend]`
182
252
  merge yields EXACTLY the local resolution (framework defaults are NOT baked —
183
253
  the shipped package applies its own). The original file is restored verbatim
184
254
  after the deploy; targets with no brand layer are already self-contained and
185
- stage nothing.
255
+ stage nothing. The config overlay follows the env overlay's environment
256
+ ([#856](https://github.com/Omega-JS-Stack/omega/issues/856)): the stage names
257
+ the environment it is staging FOR, so `omega.<environment>.json5` and
258
+ `.env.<environment>` compose for the same word (a deploy `production`, the
259
+ emulator `development`, a test lane `testing`) and no development override can
260
+ ride a production upload.
186
261
 
187
262
  ## Content-publish implies deploy
188
263
 
189
- `POST/PUT /admin/post` commit the article to the website repo, then dispatch
190
- its `build.yml` on the repo's default branch (shared helper
264
+ `POST/PUT /admin/post` commit the article to the brand's SOURCE repo
265
+ (`<brand.id>-omega`, where the site is authored, never the website repo a
266
+ built site is pushed to, [#883](https://github.com/Omega-JS-Stack/omega/issues/883)),
267
+ then dispatch its `build.yml` (shared helper
191
268
  `routes/admin/post/dispatch-deploy.js`). `settings.deploy: false` opts out;
192
269
  the outcome lands in the response as `deployDispatched` (dispatch failure is
193
- non-fatal — the post is committed either way, with a warning logged).
270
+ non-fatal: the post is committed either way, with a warning logged).
271
+
272
+ **Every publish writes BOTH branches and dispatches the DEPLOY one**
273
+ ([#919](https://github.com/Omega-JS-Stack/omega/issues/919)). All three writers
274
+ the admin routes have go to the default branch as the record and then to
275
+ `omega-deploy`, which is the branch CI builds: the post CREATE (`admin/post`'s
276
+ `commitAll`, a Git Trees commit carrying the article AND its images), the post
277
+ EDIT (`uploadPost`) and the content route (`uploadContent`), the last two plain
278
+ contents-API file writes. One module owns the branch for all of them,
279
+ `routes/admin/lib/deploy-branch.js`: it reads `SNAPSHOT_REF` from devkit rather
280
+ than typing the name, holds the ONE branch-exists read (`git.getRef`) that the
281
+ dispatch asks as well, and prints the one warning. A file write reads the file's
282
+ own sha from the DEPLOY branch, and the tree write builds on that branch's head
283
+ tree with the blobs the default-branch commit already uploaded (a blob is a
284
+ repo-level object, so nothing uploads twice), because the two branches hold
285
+ different commits and a tree cut from the wrong base would deploy the other
286
+ branch's files away. The dispatch then names `omega-deploy` too, which is what
287
+ makes an admin publish build on a brand running LOCAL framework packages: the
288
+ tarballs, the rewritten manifests and the regenerated lockfile the last local
289
+ deploy pushed are only ever on that branch, so the runner installs the same
290
+ framework the site runs on. A brand nobody has deployed locally yet has no such
291
+ branch: the write lands on the default branch alone with `deployBranch: false`,
292
+ the dispatch REFUSES with `no deploy branch yet: run omega deploy once from the
293
+ brand`, and `deployDispatched` is false. There is no fallback to building the
294
+ default branch, because that build would install `file:` specs naming folders
295
+ the runner does not have.
194
296
 
195
297
  ## HTTP surface
196
298
 
@@ -202,12 +304,13 @@ Authorization: Bearer <token>
202
304
  {"ref":"main"}
203
305
  ```
204
306
 
205
- `repository_dispatch` (`event_type: omega-deploy`) triggers the same
206
- workflows — the channel the future CMS/hosted-company surface rides (D9).
307
+ That is the ONE way in ([#923](https://github.com/Omega-JS-Stack/omega/issues/923)),
308
+ the future CMS/hosted-company surface (D9) included: the `ref` picks the
309
+ checkout, which is the whole point, and only `workflow_dispatch` carries one.
207
310
 
208
311
  ## Proven (cp98)
209
312
 
210
- Dry-runs from omega-brand print the exact dispatch for all three
313
+ Dry-runs from omega-omega print the exact dispatch for all three
211
314
  workflow-backed targets; a REAL deliberate deploy shipped hosting to the
212
315
  playground: `omega deploy --only hosting` (from `targets/backend`) → https://omegajs-playground.web.app
213
316
  serves 200 (functions stay Blaze-gated; live Actions runs stay Ian-gated on