@omega.js/desktop 0.53.0 → 0.54.1

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 (161) hide show
  1. package/README.md +38 -38
  2. package/dist/cli-run.js +4 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/cdp/client.js +1 -1
  5. package/dist/commands/cdp.js +1 -1
  6. package/dist/commands/clean.js +2 -3
  7. package/dist/commands/dev.js +25 -0
  8. package/dist/commands/lib/ensure-target.js +12 -17
  9. package/dist/commands/lib/migrate.js +17 -0
  10. package/dist/commands/logs.js +1 -1
  11. package/dist/commands/release.js +1 -1
  12. package/dist/commands/test.js +4 -4
  13. package/dist/commands/update.js +5 -4
  14. package/dist/defaults/.github/workflows/build.yml +18 -18
  15. package/dist/defaults/_.gitignore +0 -2
  16. package/dist/defaults/_mas/README.md +3 -3
  17. package/dist/defaults/config/certs/README.md +1 -1
  18. package/dist/defaults/config/omega.json5 +36 -36
  19. package/dist/defaults/docs/README.md +3 -3
  20. package/dist/defaults/gulpfile.js +1 -1
  21. package/dist/defaults/hooks/build/post.js +1 -1
  22. package/dist/defaults/hooks/build/pre.js +1 -1
  23. package/dist/defaults/hooks/notarize/post.js +2 -2
  24. package/dist/defaults/hooks/release/post.js +1 -1
  25. package/dist/defaults/hooks/release/pre.js +1 -1
  26. package/dist/defaults/src/assets/scss/pages/about.scss +1 -1
  27. package/dist/defaults/src/assets/scss/pages/main.scss +1 -1
  28. package/dist/defaults/src/assets/scss/pages/settings.scss +1 -1
  29. package/dist/defaults/src/integrations/context-menu/index.js +11 -11
  30. package/dist/defaults/src/integrations/menu/index.js +5 -5
  31. package/dist/defaults/src/integrations/tray/index.js +9 -9
  32. package/dist/defaults/src/main.js +2 -2
  33. package/dist/defaults/src/preload.js +1 -1
  34. package/dist/defaults/test/README.md +3 -3
  35. package/dist/defaults/test/_init.js +1 -1
  36. package/dist/gulp/tasks/audit.js +5 -8
  37. package/dist/lib/restart-manager/index.js +1 -1
  38. package/dist/lib/restart-manager/install.js +1 -1
  39. package/dist/lib/restart-manager/protocol.js +1 -1
  40. package/dist/main.js +4 -3
  41. package/dist/preload.js +1 -1
  42. package/dist/test/suites/build/audit.test.js +20 -7
  43. package/dist/test/suites/build/build-workflow-jobs.test.js +2 -2
  44. package/dist/test/suites/build/cli.test.js +28 -0
  45. package/dist/test/suites/build/defaults-em-dash.test.js +22 -0
  46. package/dist/test/suites/build/defaults-scaffold.test.js +19 -5
  47. package/dist/test/suites/build/deploy-direct.test.js +7 -5
  48. package/dist/test/suites/build/deploy-dispatch.test.js +2 -1
  49. package/dist/test/suites/build/deploy-hook.test.js +4 -2
  50. package/dist/test/suites/build/dev-verb.test.js +67 -0
  51. package/dist/test/suites/build/ensure-target.test.js +11 -3
  52. package/dist/test/suites/build/merge-line-files.test.js +6 -6
  53. package/dist/test/suites/build/migrate.test.js +29 -0
  54. package/dist/test/suites/build/project-scripts-deps.test.js +6 -10
  55. package/dist/test/suites/build/runner-env-write.test.js +73 -0
  56. package/dist/test/suites/build/runner.test.js +9 -8
  57. package/dist/test/suites/build/setup-scripts.test.js +27 -0
  58. package/dist/test/suites/build/validate-config.test.js +13 -2
  59. package/dist/test/suites/build/verb-logs.test.js +20 -0
  60. package/dist/test/suites/renderer/window-desktop-surface.test.js +1 -1
  61. package/dist/utils/build-pipeline.js +4 -4
  62. package/dist/utils/runner-env.js +13 -28
  63. package/dist/vendor/config/company.js +46 -14
  64. package/dist/vendor/config/defaults.js +30 -7
  65. package/dist/vendor/config/edit.js +25 -3
  66. package/dist/vendor/config/env-delivery.js +1 -1
  67. package/dist/vendor/config/env-schema.js +3 -6
  68. package/dist/vendor/config/env.js +34 -22
  69. package/dist/vendor/config/index.js +13 -17
  70. package/dist/vendor/config/load.js +15 -7
  71. package/dist/vendor/config/repo.js +10 -27
  72. package/dist/vendor/config/schema-client.js +64 -0
  73. package/dist/vendor/config/schema-cloud.js +38 -0
  74. package/dist/vendor/config/schema-manager.js +118 -0
  75. package/dist/vendor/config/schema-overrides.js +68 -0
  76. package/dist/vendor/config/schema.js +99 -152
  77. package/dist/vendor/config/validate.js +97 -77
  78. package/dist/vendor/devkit/agents-md.js +233 -0
  79. package/dist/vendor/devkit/attach-log-file.js +15 -1
  80. package/dist/vendor/devkit/ci-workflows.js +30 -30
  81. package/dist/vendor/devkit/cli-router.js +13 -7
  82. package/dist/vendor/devkit/defaults-engine.js +9 -43
  83. package/dist/vendor/devkit/deploy-snapshot.js +44 -9
  84. package/dist/vendor/devkit/env-lines.js +183 -0
  85. package/dist/vendor/devkit/local.js +62 -10
  86. package/dist/vendor/devkit/lockfile.js +32 -13
  87. package/dist/vendor/devkit/logger.js +7 -2
  88. package/dist/vendor/devkit/merge-line-files.js +219 -176
  89. package/dist/vendor/devkit/omega-bin.js +208 -111
  90. package/dist/vendor/devkit/preludes/docs-sync.js +52 -0
  91. package/dist/vendor/devkit/preludes/index.js +1 -0
  92. package/dist/vendor/devkit/target-picker.js +45 -0
  93. package/dist/vendor/devkit/test/dashed-files.js +37 -0
  94. package/dist/vendor/devkit/test/run-verb-under-tee.js +71 -0
  95. package/dist/vendor/devkit/update.js +15 -15
  96. package/dist/vendor/devkit/verb-scripts.js +40 -0
  97. package/dist/vendor/devkit/verbs.js +170 -0
  98. package/package.json +18 -24
  99. package/dist/commands/install.js +0 -37
  100. package/dist/defaults/AGENTS.md +0 -119
  101. package/dist/defaults/CLAUDE.md +0 -1
  102. package/dist/vendor/config/env-retired.js +0 -137
  103. package/dist/vendor/config/retired-keys.js +0 -635
  104. package/docs/analytics.md +0 -140
  105. package/docs/app-state.md +0 -92
  106. package/docs/audit.md +0 -69
  107. package/docs/auth.md +0 -284
  108. package/docs/auto-updater.md +0 -243
  109. package/docs/boot-sequence.md +0 -44
  110. package/docs/build-system.md +0 -169
  111. package/docs/cdp-debugging.md +0 -169
  112. package/docs/common-mistakes.md +0 -21
  113. package/docs/config-schema.md +0 -120
  114. package/docs/context-menu.md +0 -112
  115. package/docs/context.md +0 -81
  116. package/docs/css.md +0 -84
  117. package/docs/deep-link.md +0 -186
  118. package/docs/environment-detection.md +0 -112
  119. package/docs/fontawesome.md +0 -109
  120. package/docs/hooks.md +0 -89
  121. package/docs/icons.md +0 -79
  122. package/docs/index.md +0 -328
  123. package/docs/installer-options.md +0 -165
  124. package/docs/ipc.md +0 -61
  125. package/docs/lib-modules.md +0 -53
  126. package/docs/logging.md +0 -227
  127. package/docs/menu.md +0 -160
  128. package/docs/releasing.md +0 -239
  129. package/docs/remote-config.md +0 -118
  130. package/docs/remote-scripts.md +0 -144
  131. package/docs/restart-manager.md +0 -144
  132. package/docs/runner.md +0 -290
  133. package/docs/sentry.md +0 -97
  134. package/docs/shared/agent-docs.md +0 -89
  135. package/docs/shared/analytics.md +0 -612
  136. package/docs/shared/brands.md +0 -57
  137. package/docs/shared/breaking-changes.md +0 -917
  138. package/docs/shared/config.md +0 -1948
  139. package/docs/shared/deploys.md +0 -341
  140. package/docs/shared/icons.md +0 -219
  141. package/docs/shared/local-dev.md +0 -167
  142. package/docs/shared/logging.md +0 -205
  143. package/docs/shared/monitoring.md +0 -167
  144. package/docs/shared/publishing.md +0 -187
  145. package/docs/shared/rulings.md +0 -34
  146. package/docs/shared/testing.md +0 -147
  147. package/docs/shared/theming.md +0 -629
  148. package/docs/shared/translation.md +0 -342
  149. package/docs/shared/updates.md +0 -61
  150. package/docs/signing.md +0 -293
  151. package/docs/startup.md +0 -142
  152. package/docs/storage.md +0 -59
  153. package/docs/templating.md +0 -101
  154. package/docs/test-boot-layer.md +0 -157
  155. package/docs/test-framework.md +0 -362
  156. package/docs/themes.md +0 -149
  157. package/docs/tooltips.md +0 -99
  158. package/docs/tray.md +0 -164
  159. package/docs/usage.md +0 -58
  160. package/docs/verts.md +0 -62
  161. package/docs/windows.md +0 -149
@@ -1,341 +0,0 @@
1
- # Deliberate deploys (D13 / D9 addendum)
2
-
3
- **Commits never auto-publish.** Save = commit; publish = an explicit deploy
4
- action. Every entry surface — the local CLI, a server-side content action
5
- (admin post), or a plain HTTP call — converges on ONE executor:
6
- `@omega.js/devkit/deploy`.
7
-
8
- ## The executor — `@omega.js/devkit/deploy`
9
-
10
- A GitHub REST `workflow_dispatch` client on native fetch (no gh-CLI
11
- dependency — Cloud Functions and laptops share the code path). Vendored into
12
- framework dists like every devkit module.
13
-
14
- | Export | Contract |
15
- |--------|----------|
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 `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 |
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 |
22
- | `buildDispatch({ owner, repo, workflow, ref, inputs })` | the PLAN: `{ method, url, body, runsUrl }` — dry-run output is exactly this |
23
- | `dispatchWorkflow(plan, { token, fetchFn })` | POST to `/repos/{o}/{r}/actions/workflows/{wf}/dispatches`; 204 = accepted, anything else throws with status + body |
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.
53
-
54
- ## Scaffolded workflows — no push triggers
55
-
56
- | Target | Workflow | Triggers | Publishes |
57
- |--------|----------|----------|-----------|
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 ci`, the target `.env` written from the generated secrets block, the service-account file, the Firebase CLI, `gcloud` authenticated with that file, then the verb) |
62
-
63
- Existing consumers converge on their next omega verb (both scaffold
64
- engines treat workflow files as framework-owned overwrites).
65
-
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)).
67
-
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.
75
-
76
- **One install step on every lane: `sfw npm ci {{ installWorkspace }}`** ([#938](https://github.com/Omega-JS-Stack/omega/issues/938)): all four templates install with `npm ci`, composed in a brand as `sfw npm ci --workspace .` (desktop's cmd-shelled Windows legs spell it `sfw npm.cmd ci`, and its self-hosted signing box runs the same `npm.cmd ci` without the firewall, above). `npm ci` installs the brand lockfile exactly as the snapshot carried it and REFUSES one that disagrees with the manifests, where `npm install` re-resolves around it: that difference is how one 0.51.0 deploy saw web follow a stale lock's local-era link entries and never land its bin while desktop, the one template already on `npm ci`, refused the same lock. The lockfile the runner installs is therefore the deploy's contract, and the registry lane gates it before anything is pushed (below). `ci-workflows.test.js` pins the install line on all four templates.
77
-
78
- **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.
79
-
80
- **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)).
81
-
82
- **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.
83
-
84
- **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.
85
-
86
- **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.
87
-
88
- **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 or a CI run skips loudly, and a checkout whose `origin` is not the derived source repo REFUSES on the one drift line of the [origin gate](#the-origin-gate-934), because arming another repo's Actions with this brand's secrets is the harm itself (a nested brand, whose remote is the enclosing repo's by construction, publishes to the derived repo without the check). **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).
89
-
90
- ## The mac signing ladder fails loudly at every rung ([#891](https://github.com/Omega-JS-Stack/omega/issues/891))
91
-
92
- 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).
93
-
94
- | Rung | Where | What it does now |
95
- |---|---|---|
96
- | 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)) |
97
- | 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)) |
98
- | 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` |
99
- | 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 |
100
- | 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)) |
101
-
102
- 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.
103
-
104
- ## Publish: assets, stores, and the manual step
105
-
106
- **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`):
107
-
108
- | Format | What a publish does |
109
- |---|---|
110
- | `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` |
111
- | `kind: 'store'` with its keys (chrome, firefox, edge, snap) | Published to the store |
112
- | `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 |
113
- | `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 |
114
-
115
- **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.
116
-
117
- **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.
118
-
119
- ## The verb — `omega deploy` on every target
120
-
121
- 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.
122
-
123
- **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.
124
-
125
- **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>)`.
126
-
127
- **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.
128
-
129
- | Target | Dispatch lane | `--direct` | Flags |
130
- |--------|---------------|------------|-------|
131
- | 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` |
132
- | 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` |
133
- | 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` |
134
- | 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) |
135
- | 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 | — |
136
-
137
- **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, a lockfile that disagrees with the manifests, or an empty required secret.
138
-
139
- 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).
140
-
141
- **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.
142
-
143
- ### The brand-root fan-out: `omega deploy` at a brand root (cp251)
144
-
145
- 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.
146
-
147
- - **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.
148
- - **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.
149
- - **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.
150
- - **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.
151
- - **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.
152
- - **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=`.
153
- - **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`.
154
- - **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.
155
- - **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.
156
- - **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).
157
-
158
- #### The container lane — a custom-server backend ([#584](https://github.com/Omega-JS-Stack/omega/issues/584))
159
-
160
- A backend declaring `targets.backend.projectType: 'custom'` runs its Express app on `PORT` for a container host (Render & co) instead of exporting Cloud Functions, so there is no `firebase deploy` to fan out to — and its framework verb refuses. The fan-out takes the SCRIPT lane instead, exactly as it does for a custom TARGET ([#603](https://github.com/Omega-JS-Stack/omega/issues/603)); the one home of that decision is the manager's `resolveTargetRun`, so the two lanes cannot drift.
161
-
162
- - **`npm run deploy` in the target dir** — whatever the brand put there IS the deploy: `render deploys create --service-id …`, a `git push` at the host's connected branch, a `docker` build-and-push. OMEGA does not name the host's command, because the host is the brand's choice, not the framework's.
163
- - **No framework flags are forwarded** (a package script has no contract for them), and for the same reason `--dry-run` stops at the PLAN rather than running a script that could not honor it.
164
- - **No `deploy` script = a loud skip**, naming the target and the verb. Nothing is marked failed: an absent script is a brand that has not wired its host yet, not a broken deploy.
165
- - **Order is unchanged** — the backend still deploys first, container or not; the surfaces that call the API go after it.
166
- - 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.
167
-
168
- **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.
169
-
170
- ## One command, repo to live — manage + deploy + verify ([#48](https://github.com/Omega-JS-Stack/omega/issues/48))
171
-
172
- A brand goes from repo to live through the EXISTING verbs, not a new one:
173
- `omega pipeline --deploy=web,backend` at a brand root runs the full manage
174
- cycle (provisioning every service), then each target's deploy leg, then the
175
- **verify sweep** — the last mile that proves the launch surface actually
176
- answers from the outside:
177
-
178
- | Check | What it proves | How |
179
- |---|---|---|
180
- | `verify:site` | the canonical URL serves a real page | HTTPS GET on `brand.url` → 200 + `text/html` + a non-trivial body (a 200 empty shell fails) |
181
- | `verify:domain` | the names resolve | DNS resolution of the apex (plus `www` when the brand owns the apex; a subdomain brand checks only its own host) |
182
- | `verify:cloudflare` | traffic arrives proxied, not origin-direct | `cf-ray` header, or `server: cloudflare` |
183
-
184
- Checks land in the pipeline scorecard as `verify:<name>` rows and are judged
185
- exactly like deploy legs — a failed check fails the run. The sweep runs
186
- automatically for whatever was deployed (`--deploy=web` → all three);
187
- `--verify` alone runs it WITHOUT deploying (the post-hoc "is it still up?"
188
- pass). Three states gate the sweep — a dry run (plan-only, like every other
189
- manager verb), a demo-* (emulator-only) brand, and a brand with no cloud
190
- project at all: each check records a gated skip with its reason and no
191
- transport is called, so the offline brands stay offline. Home: `packages/manager/src/lib/verify-live.js`
192
- (both transports — fetch and DNS — are injected seams, so the tests run
193
- fully offline).
194
-
195
- ## Local frameworks in production artifacts: the ONE pack step ([#872](https://github.com/Omega-JS-Stack/omega/issues/872))
196
-
197
- 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 ci` against the lockfile the pack step regenerated. No clone, no `preinstall`, nothing framework-aware on the box.
198
-
199
- ### The lane matrix
200
-
201
- ONE lane since [#915](https://github.com/Omega-JS-Stack/omega/issues/915), and
202
- the two refusals it still owes:
203
-
204
- | The brand tree | Mode | Ref | Why |
205
- |---|---|---|---|
206
- | 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 lockfile gate for an unlinked one, the secrets publisher reads `nested`), never as a second lane |
207
- | **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` |
208
- | 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 |
209
-
210
- ### The pack step
211
-
212
- `@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.
213
-
214
- - **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).
215
- - **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).
216
- - **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 through devkit's ONE lock helper, `regenerateLockfile({ root })` in `src/lockfile.js` (`npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix <root>`, through `safeInstall`; the same helper `omega i live` and `omega i local` call, [#938](https://github.com/Omega-JS-Stack/omega/issues/938)), so lock regeneration never asks the registry for an unpublished package.
217
- - **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.
218
- - **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.
219
-
220
- 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.
221
-
222
- ### The registry lane's lockfile gate ([#938](https://github.com/Omega-JS-Stack/omega/issues/938))
223
-
224
- A registry (unlinked) snapshot ships the brand's OWN `package-lock.json` untouched, and the runner's `npm ci` installs exactly what it says. So before anything else in that lane but the [origin gate](#the-origin-gate-934) (before the default branch is read or healed, before a workflow file or the snapshot is pushed) `assertBrandLockfile({ root })` in `@omega.js/devkit/brand-version`, beside the version drift gate, reads the lock against every `@omega.js/*` registry spec in the brand root's `package.json` and each `targets/*/package.json`:
225
-
226
- - **The lock must exist.** A brand with none refuses (`<root> has no package-lock.json`).
227
- - **Each dependency must be locked as a REGISTRY entry**, found where npm would resolve it (the target's own `targets/<name>/node_modules/<pkg>` first, then the hoisted `node_modules/<pkg>`): not absent, not `link: true` (the local era's symlink into a checkout), and not a `resolved` that is a path (a packed `file:` tarball or a directory) rather than a registry URL.
228
- - **At a version that satisfies the manifest's spec** (exact, `^`, `~`, `>=`, x-ranges and `*`, read by devkit's `update.js` range reader), so a lock at 0.50.0 under a 0.51.0 spec refuses.
229
- - **No stale lock entry for a folder no longer on disk declares a non-registry `@omega.js` spec.** A renamed target leaves its old workspace entry behind (`targets/website` after the move to `targets/web`), and npm keeps honoring the `file:` spec it declares, re-creating the link on the next lock-only run.
230
-
231
- The refusal lists every disagreeing entry as `<manifest>: <package> <spec> <why>` and names the one fix: **run `omega i live` in the brand**, which regenerates the brand's own lockfile from its manifests ([local-dev.md](local-dev.md)). It runs on `--dry-run` too, because it reads only disk, the same as the version gate. The linked lane never runs it: its pack step deletes and regenerates the lock for the staged shape, and a brand with no repo pushes no lockfile at all. In a brand-root fan-out the gate runs once, in the run's one delivery, and each target's dry run runs it for itself.
232
-
233
- ### The origin gate ([#934](https://github.com/Omega-JS-Stack/omega/issues/934))
234
-
235
- Every lane acts on the DERIVED source repo, `<brand.id>-omega` under `repo.org`: the workflow compose, the snapshot push and the dispatch all address it. A checkout whose `origin` names another repo (a transfer or a rename nobody carried into the config) would push the mirror to one repo while the clone points at another, silently. So before anything else in every lane, the lockfile gate included, `assertOriginMatches({ dir })` in `@omega.js/devkit/git-remote` reads the brand's OWN `origin` and compares its whole slug with `sourceRepo(config).slug` through `@omega.js/config`'s `repoDrift` (case-insensitive, GitHub's own comparison), against the brand's production config, the one every dispatch address reads. A mismatch refuses with the one line the boot prelude also prints:
236
-
237
- ```
238
- origin is <slug> but config derives <derived>: fix repo.org in config/omega.json5 or move the repo
239
- ```
240
-
241
- It runs on `--dry-run` too, because it only reads. It runs on the dispatch-only lane as well, since that dispatch still addresses the derived repo; a brand outside git has no origin to disagree, which the gate answers as nothing to compare. The same holds for no `.git` AT the brand root (a nested brand such as `brands/playground-omega`, whose git toplevel is somebody else's repo), no `origin` at all (a brand nobody has pushed yet), and a non-GitHub remote. In a brand-root fan-out the gate runs once, in the run's one delivery, and each target's dry run runs it for itself. `omega manage` refuses on the same line, in the repo service before any ensure ([manager/repo.md](../manager/repo.md)), and so does the secrets precheck `publishTargetSecrets` (each framework's deploy precheck, and the walk's `secrets` op), through the same `assertOriginMatches`, where it used to warn and skip on a comparison of its own. Which gate speaks first depends on the run, and both print the same line: in a brand-root fan-out the run's one delivery lane refuses before any target's precheck starts; in a single-target `omega deploy` the secrets precheck runs ahead of that target's lane and refuses first.
242
-
243
- ### The snapshot push
244
-
245
- `deployViaDispatch` runs the lane in this order: the origin gate above; on a registry (unlinked) lane, the lockfile gate above; 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.
246
-
247
- **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.
248
-
249
- **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.
250
-
251
- - **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.
252
- - **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.
253
- - **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.
254
- - **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.
255
-
256
- ### A self-hosted-runner workflow fires only on dispatch
257
-
258
- 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).
259
-
260
- **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.
261
-
262
- **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.
263
-
264
- ### Backend: resolved-config staging (#31)
265
-
266
- The config cascade (company ← brand ← local) is a walk-up over the local tree,
267
- and the upload boundary cuts it — Cloud Functions receives only the functions
268
- folder with its slim targets-only local config, so a deployed backend used to
269
- serve framework defaults ("My Brand"). Alongside package staging, the deploy
270
- command stages the resolved config (`src/cli/utils/stage-resolved-config.js`):
271
- `@omega.js/config`'s `composeTargetConfig(functionsPath, 'backend')` freezes
272
- the full interleave (brand shared ← brand target ← local shared ← local target)
273
- into the staged local file's shared namespace with a presence-only `targets`
274
- map, so the deployed runtime's own `defaults ← shared ← targets[backend]`
275
- merge yields EXACTLY the local resolution (framework defaults are NOT baked —
276
- the shipped package applies its own). The original file is restored verbatim
277
- after the deploy; targets with no brand layer are already self-contained and
278
- stage nothing. The config overlay follows the env overlay's environment
279
- ([#856](https://github.com/Omega-JS-Stack/omega/issues/856)): the stage names
280
- the environment it is staging FOR, so `omega.<environment>.json5` and
281
- `.env.<environment>` compose for the same word (a deploy `production`, the
282
- emulator `development`, a test lane `testing`) and no development override can
283
- ride a production upload.
284
-
285
- ## Content-publish implies deploy
286
-
287
- `POST/PUT /admin/post` commit the article to the brand's SOURCE repo
288
- (`<brand.id>-omega`, where the site is authored, never the website repo a
289
- built site is pushed to, [#883](https://github.com/Omega-JS-Stack/omega/issues/883)),
290
- then dispatch its `build.yml` (shared helper
291
- `routes/admin/post/dispatch-deploy.js`). `settings.deploy: false` opts out;
292
- the outcome lands in the response as `deployDispatched` (dispatch failure is
293
- non-fatal: the post is committed either way, with a warning logged).
294
-
295
- **Every publish writes BOTH branches and dispatches the DEPLOY one**
296
- ([#919](https://github.com/Omega-JS-Stack/omega/issues/919)). All three writers
297
- the admin routes have go to the default branch as the record and then to
298
- `omega-deploy`, which is the branch CI builds: the post CREATE (`admin/post`'s
299
- `commitAll`, a Git Trees commit carrying the article AND its images), the post
300
- EDIT (`uploadPost`) and the content route (`uploadContent`), the last two plain
301
- contents-API file writes. One module owns the branch for all of them,
302
- `routes/admin/lib/deploy-branch.js`: it reads `SNAPSHOT_REF` from devkit rather
303
- than typing the name, holds the ONE branch-exists read (`git.getRef`) that the
304
- dispatch asks as well, and prints the one warning. A file write reads the file's
305
- own sha from the DEPLOY branch, and the tree write builds on that branch's head
306
- tree with the blobs the default-branch commit already uploaded (a blob is a
307
- repo-level object, so nothing uploads twice), because the two branches hold
308
- different commits and a tree cut from the wrong base would deploy the other
309
- branch's files away. The dispatch then names `omega-deploy` too, which is what
310
- makes an admin publish build on a brand running LOCAL framework packages: the
311
- tarballs, the rewritten manifests and the regenerated lockfile the last local
312
- deploy pushed are only ever on that branch, so the runner installs the same
313
- framework the site runs on. A brand nobody has deployed locally yet has no such
314
- branch: the write lands on the default branch alone with `deployBranch: false`,
315
- the dispatch REFUSES with `no deploy branch yet: run omega deploy once from the
316
- brand`, and `deployDispatched` is false. There is no fallback to building the
317
- default branch, because that build would install `file:` specs naming folders
318
- the runner does not have.
319
-
320
- ## HTTP surface
321
-
322
- Any caller with a repo-scoped token can deploy without OMEGA installed:
323
-
324
- ```
325
- POST https://api.github.com/repos/<owner>/<repo>/actions/workflows/build.yml/dispatches
326
- Authorization: Bearer <token>
327
- {"ref":"main"}
328
- ```
329
-
330
- That is the ONE way in ([#923](https://github.com/Omega-JS-Stack/omega/issues/923)),
331
- the future CMS/hosted-company surface (D9) included: the `ref` picks the
332
- checkout, which is the whole point, and only `workflow_dispatch` carries one.
333
-
334
- ## Proven (cp98)
335
-
336
- Dry-runs from omega-omega print the exact dispatch for all three
337
- workflow-backed targets; a REAL deliberate deploy shipped hosting to the
338
- playground: `omega deploy --only hosting` (from `targets/backend`) → https://omegajs-playground.web.app
339
- serves 200 (functions stay Blaze-gated; live Actions runs stay Ian-gated on
340
- runner minutes). Suites: devkit deploy 7, corpus 1225, desktop 761, ext 99,
341
- web 80.