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