@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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deploy-follow: follow a dispatched GitHub Actions run to its verdict
|
|
3
|
+
* ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)).
|
|
4
|
+
*
|
|
5
|
+
* A dispatch used to end at the runs page URL: the verb printed a link and
|
|
6
|
+
* exited 0 while the run it had just started went red a few minutes later. So
|
|
7
|
+
* every dispatched deploy follows its run instead, through this ONE follower
|
|
8
|
+
* (it was desktop's alone, inside `omega release`, on `@octokit/rest`): the run
|
|
9
|
+
* and its jobs are polled, each job's log is streamed with its name in front,
|
|
10
|
+
* and the verb's exit code is the run's conclusion.
|
|
11
|
+
*
|
|
12
|
+
* Why poll instead of stream? GitHub Actions exposes no live stdout: a job's
|
|
13
|
+
* log is fetchable only as each STEP closes, so "streaming" is a polite fiction
|
|
14
|
+
* where every tick re-fetches each job's log and prints the bytes it has not
|
|
15
|
+
* printed yet.
|
|
16
|
+
*
|
|
17
|
+
* A poll failure is an expected external condition, never a verdict: a connect
|
|
18
|
+
* timeout on one tick used to mark a deploy failed on the laptop and stop a
|
|
19
|
+
* brand fan-out while the run went green. MAX_TRANSIENT_POLLS failures IN A ROW
|
|
20
|
+
* rethrow; anything short of that waits and polls again.
|
|
21
|
+
*
|
|
22
|
+
* Everything prints through the caller's logger, so the lines carry that verb's
|
|
23
|
+
* tag and land in whatever file the verb teed (`logs/deploy.log`:
|
|
24
|
+
* [docs/shared/logging.md](../../../docs/shared/logging.md)).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Libraries
|
|
28
|
+
const { setTimeout: delay } = require('node:timers/promises');
|
|
29
|
+
const { ghHeaders, shortSha } = require('./deploy-snapshot.js');
|
|
30
|
+
|
|
31
|
+
// Constants
|
|
32
|
+
const API_BASE = 'https://api.github.com';
|
|
33
|
+
// One tick of the follow. Job logs only change as steps close, so a faster tick
|
|
34
|
+
// buys nothing but rate limit.
|
|
35
|
+
const POLL_INTERVAL_MS = 5000;
|
|
36
|
+
// GitHub registers a dispatched run a few seconds after it accepts the
|
|
37
|
+
// dispatch: a minute of 2s looks, then a loud failure naming the workflow.
|
|
38
|
+
const RUN_LOOKUP_MS = 60_000;
|
|
39
|
+
const RUN_LOOKUP_INTERVAL_MS = 2000;
|
|
40
|
+
// Consecutive polls the network may fail before the follower gives up: a minute
|
|
41
|
+
// of 5s ticks. The runner keeps building whether or not this laptop can reach
|
|
42
|
+
// api.github.com for one tick, so one timeout must never end the follow.
|
|
43
|
+
const MAX_TRANSIENT_POLLS = 12;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A job's verdict as one character. A SKIPPED job is not a failed one: a run
|
|
47
|
+
* that ships two of three platforms skips the legs its `platforms` input left
|
|
48
|
+
* out, and the signing leg skips with them, so a cross there reads as three
|
|
49
|
+
* failures beside a green run.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} job - The job, as GitHub reports it (`status`, `conclusion`).
|
|
52
|
+
* @returns {string} The symbol for the banner.
|
|
53
|
+
*/
|
|
54
|
+
function jobSymbol(job) {
|
|
55
|
+
if (job.status === 'completed') {
|
|
56
|
+
if (job.conclusion === 'success') return '✓';
|
|
57
|
+
return job.conclusion === 'skipped' ? '⊘' : '✗';
|
|
58
|
+
}
|
|
59
|
+
if (job.status === 'in_progress') return '…';
|
|
60
|
+
if (job.status === 'queued') return '·';
|
|
61
|
+
return '?';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The one-line status banner: the run's state and every job's symbol. Printed
|
|
66
|
+
* only when it CHANGES, so a poll that found nothing new says nothing.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} run - The run state.
|
|
69
|
+
* @param {object[]} jobs - Its jobs.
|
|
70
|
+
* @returns {string} The banner line.
|
|
71
|
+
*/
|
|
72
|
+
function statusBanner(run, jobs) {
|
|
73
|
+
const parts = jobs.map((job) => `${jobSymbol(job)} ${job.name}`);
|
|
74
|
+
|
|
75
|
+
return `-- ${run.status}${run.conclusion ? ` (${run.conclusion})` : ''} -- ${parts.join(' | ')}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One GET against the API, as JSON. Throws on any non-200, for the caller's transient count. */
|
|
79
|
+
async function getJson(url, { token, fetchFn }) {
|
|
80
|
+
const response = await fetchFn(url, { headers: ghHeaders(token) });
|
|
81
|
+
|
|
82
|
+
if (response.status !== 200) {
|
|
83
|
+
throw new Error(`GET ${url} answered ${response.status}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return response.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The run this dispatch started: the newest `workflow_dispatch` run of that
|
|
91
|
+
* workflow created at or after the dispatch (less a second of clock skew).
|
|
92
|
+
*
|
|
93
|
+
* @param {object} options - Options.
|
|
94
|
+
* @param {string} options.owner - The repo owner.
|
|
95
|
+
* @param {string} options.repo - The repo's bare name.
|
|
96
|
+
* @param {string} options.workflow - The workflow file the dispatch named.
|
|
97
|
+
* @param {number} options.sinceMs - The instant read before the dispatch.
|
|
98
|
+
* @param {string} options.token - The GitHub token.
|
|
99
|
+
* @param {Function} options.fetchFn - The fetch seam.
|
|
100
|
+
* @param {Function} options.sleepFn - The wait seam.
|
|
101
|
+
* @param {number} options.deadlineMs - How long to look for it.
|
|
102
|
+
* @returns {Promise<object>} The run.
|
|
103
|
+
* @throws {Error} When no run appears in the budget.
|
|
104
|
+
*/
|
|
105
|
+
async function findRun(options) {
|
|
106
|
+
const { owner, repo, workflow, sinceMs, token, fetchFn, sleepFn, deadlineMs } = options;
|
|
107
|
+
const url = `${API_BASE}/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?event=workflow_dispatch&per_page=5`;
|
|
108
|
+
const deadline = Date.now() + deadlineMs;
|
|
109
|
+
|
|
110
|
+
while (true) {
|
|
111
|
+
let runs = [];
|
|
112
|
+
try {
|
|
113
|
+
runs = (await getJson(url, { token, fetchFn })).workflow_runs || [];
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// The listing is as transient as any other poll: the run that was just
|
|
116
|
+
// dispatched is worth the rest of the budget.
|
|
117
|
+
runs = [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The NEWEST match, never the first one listed: an older run of the same
|
|
121
|
+
// workflow is exactly what a dispatch lands beside.
|
|
122
|
+
const fresh = runs
|
|
123
|
+
.filter((run) => new Date(run.created_at).getTime() >= sinceMs - 1000)
|
|
124
|
+
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
|
|
125
|
+
|
|
126
|
+
if (fresh) return fresh;
|
|
127
|
+
|
|
128
|
+
if (Date.now() >= deadline) {
|
|
129
|
+
throw new Error(`${workflow} was dispatched on ${owner}/${repo} but no run appeared within ${Math.round(deadlineMs / 1000)}s. Check https://github.com/${owner}/${repo}/actions/workflows/${workflow}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await sleepFn(RUN_LOOKUP_INTERVAL_MS);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Follow a dispatched run to its conclusion: print each job's log as it
|
|
138
|
+
* arrives, a banner on every transition, and the verdict at the end.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} options - Options.
|
|
141
|
+
* @param {string} options.owner - The repo owner.
|
|
142
|
+
* @param {string} options.repo - The repo's bare name.
|
|
143
|
+
* @param {string} options.workflow - The workflow file the dispatch named.
|
|
144
|
+
* @param {Date|number|string} options.since - The instant read BEFORE the dispatch.
|
|
145
|
+
* @param {string} options.token - The GitHub token.
|
|
146
|
+
* @param {string} [options.headSha] - The sha this deploy pushed: a run whose
|
|
147
|
+
* head is anything else is REFUSED
|
|
148
|
+
* ([#902](https://github.com/Omega-JS-Stack/omega/issues/902)). Omitted on a
|
|
149
|
+
* push lane, whose run head is the developer's own commit.
|
|
150
|
+
* @param {object} [options.logger] - Logger with `log`/`warn` (silent when omitted).
|
|
151
|
+
* @param {Function} [options.fetchFn] - The fetch seam (tests).
|
|
152
|
+
* @param {Function} [options.sleepFn] - The wait seam (tests).
|
|
153
|
+
* @param {number} [options.pollMs] - The tick between polls.
|
|
154
|
+
* @param {number} [options.lookupMs] - How long to look for the run.
|
|
155
|
+
* @returns {Promise<{ run: object, conclusion: string }>} The completed run and its conclusion.
|
|
156
|
+
* @throws {Error} When no run appears, when the run found is building another
|
|
157
|
+
* tree than `headSha`, when the network fails MAX_TRANSIENT_POLLS times in a
|
|
158
|
+
* row, or when the run concludes as anything but success.
|
|
159
|
+
*/
|
|
160
|
+
async function followRun(options) {
|
|
161
|
+
const {
|
|
162
|
+
owner,
|
|
163
|
+
repo,
|
|
164
|
+
workflow,
|
|
165
|
+
since,
|
|
166
|
+
token,
|
|
167
|
+
headSha,
|
|
168
|
+
logger,
|
|
169
|
+
fetchFn = fetch,
|
|
170
|
+
sleepFn = delay,
|
|
171
|
+
pollMs = POLL_INTERVAL_MS,
|
|
172
|
+
lookupMs = RUN_LOOKUP_MS,
|
|
173
|
+
} = options;
|
|
174
|
+
|
|
175
|
+
const log = (line) => { if (logger) logger.log(line); };
|
|
176
|
+
const sinceMs = new Date(since).getTime();
|
|
177
|
+
|
|
178
|
+
const found = await findRun({ owner, repo, workflow, sinceMs, token, fetchFn, sleepFn, deadlineMs: lookupMs });
|
|
179
|
+
|
|
180
|
+
// The run has to be building the tree this deploy pushed (#902). GitHub
|
|
181
|
+
// resolves the dispatch's ref on its own side, so a run started in the same
|
|
182
|
+
// second as the force-push can carry the PREVIOUS snapshot, publish the
|
|
183
|
+
// version before this one, and conclude green with nothing in its log saying
|
|
184
|
+
// so. Following it at all would report that as this deploy's verdict.
|
|
185
|
+
if (headSha && found.head_sha !== headSha) {
|
|
186
|
+
throw new Error(`${workflow} started a run on ${shortSha(found.head_sha)}, but this deploy pushed ${shortSha(headSha)}: the run is building a tree this deploy did not push, so its result is not this deploy's. ${found.html_url}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
log(`Run started: ${found.html_url}${headSha ? ` (head ${shortSha(headSha)})` : ''}`);
|
|
190
|
+
|
|
191
|
+
const runUrl = `${API_BASE}/repos/${owner}/${repo}/actions/runs/${found.id}`;
|
|
192
|
+
const jobsUrl = `${runUrl}/jobs?per_page=100`;
|
|
193
|
+
// How much of each job's log has been printed, so a re-fetch prints only what
|
|
194
|
+
// the last one did not have.
|
|
195
|
+
const printedByJob = new Map();
|
|
196
|
+
let failures = 0;
|
|
197
|
+
let lastBanner = '';
|
|
198
|
+
|
|
199
|
+
while (true) {
|
|
200
|
+
let state = null;
|
|
201
|
+
let jobs = [];
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
state = await getJson(runUrl, { token, fetchFn });
|
|
205
|
+
jobs = (await getJson(jobsUrl, { token, fetchFn })).jobs || [];
|
|
206
|
+
failures = 0;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Never a verdict on the run: the runner is building either way.
|
|
209
|
+
failures += 1;
|
|
210
|
+
if (failures > MAX_TRANSIENT_POLLS) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
if (logger) {
|
|
214
|
+
logger.warn(`Poll failed (${failures}/${MAX_TRANSIENT_POLLS}, retrying): ${error.message}`);
|
|
215
|
+
}
|
|
216
|
+
await sleepFn(pollMs);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const job of jobs) {
|
|
221
|
+
// A queued job has no log yet, and asking for one is a 404 per tick.
|
|
222
|
+
if (job.status === 'queued') continue;
|
|
223
|
+
|
|
224
|
+
const text = await readJobLog({ owner, repo, jobId: job.id, token, fetchFn });
|
|
225
|
+
if (text === null) continue;
|
|
226
|
+
|
|
227
|
+
const printed = printedByJob.get(job.id) || 0;
|
|
228
|
+
if (text.length > printed) {
|
|
229
|
+
for (const line of text.slice(printed).split('\n')) {
|
|
230
|
+
if (!line) continue;
|
|
231
|
+
log(`[${job.name}] ${line}`);
|
|
232
|
+
}
|
|
233
|
+
printedByJob.set(job.id, text.length);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const banner = statusBanner(state, jobs);
|
|
238
|
+
if (banner !== lastBanner) {
|
|
239
|
+
log(banner);
|
|
240
|
+
lastBanner = banner;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (state.status === 'completed') {
|
|
244
|
+
const conclusion = state.conclusion;
|
|
245
|
+
const success = conclusion === 'success';
|
|
246
|
+
log(`${success ? '✓' : '✗'} Run ${conclusion}: ${state.html_url}`);
|
|
247
|
+
|
|
248
|
+
if (!success) {
|
|
249
|
+
throw new Error(`${workflow} concluded ${conclusion}: ${state.html_url}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { run: state, conclusion };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await sleepFn(pollMs);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* One job's log so far, or null when there is none to read yet. A job's log is
|
|
261
|
+
* 404 until its first step closes, and it arrives in chunks after that, so a
|
|
262
|
+
* miss is the ordinary case rather than a failure.
|
|
263
|
+
*
|
|
264
|
+
* @param {object} options - Options.
|
|
265
|
+
* @param {string} options.owner - The repo owner.
|
|
266
|
+
* @param {string} options.repo - The repo's bare name.
|
|
267
|
+
* @param {number} options.jobId - The job.
|
|
268
|
+
* @param {string} options.token - The GitHub token.
|
|
269
|
+
* @param {Function} options.fetchFn - The fetch seam.
|
|
270
|
+
* @returns {Promise<string|null>} The log text, or null.
|
|
271
|
+
*/
|
|
272
|
+
async function readJobLog(options) {
|
|
273
|
+
const { owner, repo, jobId, token, fetchFn } = options;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const response = await fetchFn(`${API_BASE}/repos/${owner}/${repo}/actions/jobs/${jobId}/logs`, {
|
|
277
|
+
headers: ghHeaders(token),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (response.status !== 200) return null;
|
|
281
|
+
|
|
282
|
+
return await response.text();
|
|
283
|
+
} catch (e) {
|
|
284
|
+
// A log this tick could not read is a log the next tick reads: the run's
|
|
285
|
+
// own state is what the poll above answers for.
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
followRun,
|
|
292
|
+
findRun,
|
|
293
|
+
jobSymbol,
|
|
294
|
+
statusBanner,
|
|
295
|
+
MAX_TRANSIENT_POLLS,
|
|
296
|
+
POLL_INTERVAL_MS,
|
|
297
|
+
};
|
|
@@ -10,13 +10,21 @@
|
|
|
10
10
|
* lives here and every framework file is its list.
|
|
11
11
|
*
|
|
12
12
|
* The contract the runner guarantees:
|
|
13
|
-
* - `--no-secrets` (
|
|
13
|
+
* - `--no-secrets` (parsed as `secrets: false`) skips the whole precheck, the
|
|
14
14
|
* same opt-out name on web, desktop and extension (parity; desktop's old
|
|
15
15
|
* `--quick` is gone).
|
|
16
|
-
* - Every step is SOFT: a throwing step is reported as a warning
|
|
17
|
-
* continues. A precheck reports; the deploy is the caller's
|
|
16
|
+
* - Every step is SOFT by default: a throwing step is reported as a warning
|
|
17
|
+
* and the run continues. A precheck reports; the deploy is the caller's
|
|
18
|
+
* decision. A step marked `fatal` is the exception
|
|
19
|
+
* ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)): its failure
|
|
20
|
+
* means the deploy would ship something broken (an unsigned, unnotarized
|
|
21
|
+
* mac build), so it rethrows and the deploy never dispatches.
|
|
18
22
|
* - Steps run in list order and `ran` names the ones that finished, so a test
|
|
19
23
|
* hands its own recorder in and reads back exactly what happened.
|
|
24
|
+
* - A DRY RUN still runs the precheck
|
|
25
|
+
* ([#895](https://github.com/Omega-JS-Stack/omega/issues/895)): every step
|
|
26
|
+
* is a read or a plan under `dryRun`, and a precheck that only ran for real
|
|
27
|
+
* deploys was a preview nobody could trust. Each step receives it.
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -24,11 +32,12 @@
|
|
|
24
32
|
* @param {string} input.projectDir - The target root.
|
|
25
33
|
* @param {object} [input.options] - The parsed CLI options (`secrets: false` = opted out).
|
|
26
34
|
* @param {object} input.logger - `{ log, warn, error }`.
|
|
27
|
-
* @param {
|
|
35
|
+
* @param {boolean} [input.dryRun] - Plan only; handed to every step.
|
|
36
|
+
* @param {Array<{ name: string, run: function, fatal?: boolean }>} input.steps - The framework's step list.
|
|
28
37
|
* @returns {Promise<{ skipped: string }|{ ran: string[] }>} The steps that ran, or the opt-out marker.
|
|
29
38
|
*/
|
|
30
|
-
async function runDeployPrecheck({ projectDir, options, logger, steps }) {
|
|
31
|
-
//
|
|
39
|
+
async function runDeployPrecheck({ projectDir, options, logger, steps, dryRun }) {
|
|
40
|
+
// The parse turns `--no-secrets` into `secrets: false` (src/argv.js).
|
|
32
41
|
if (options && options.secrets === false) {
|
|
33
42
|
logger.log('Skipping the network prechecks (--no-secrets)');
|
|
34
43
|
return { skipped: 'opt-out' };
|
|
@@ -40,10 +49,16 @@ async function runDeployPrecheck({ projectDir, options, logger, steps }) {
|
|
|
40
49
|
|
|
41
50
|
for (const step of steps) {
|
|
42
51
|
try {
|
|
43
|
-
await step.run({ projectDir, log, warn });
|
|
52
|
+
await step.run({ projectDir, log, warn, dryRun: dryRun === true });
|
|
44
53
|
ran.push(step.name);
|
|
45
54
|
} catch (e) {
|
|
46
|
-
// A
|
|
55
|
+
// A FATAL step guards something the deploy would otherwise ship broken:
|
|
56
|
+
// it stops the run instead of narrating it (#891).
|
|
57
|
+
if (step.fatal) {
|
|
58
|
+
throw new Error(`${step.name} failed during the deploy precheck: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Everything else REPORTS; the deploy is the caller's decision.
|
|
47
62
|
warn(`${step.name} failed during the deploy precheck (non-fatal): ${e.message}`);
|
|
48
63
|
}
|
|
49
64
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The per-brand deploy record
|
|
3
|
-
* `.omega/state.json` (gitignored, per-machine).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The per-brand deploy record: one key per target NAME under the `deploy`
|
|
3
|
+
* section of `.omega/state.json` (gitignored, per-machine). A brand running
|
|
4
|
+
* several targets of one type records each under its own name (#886), so
|
|
5
|
+
* targets/web and targets/admin never share a stamp.
|
|
6
6
|
*
|
|
7
7
|
* Written by every framework's deploy verb on success; read by the
|
|
8
8
|
* manager's testing service to tell "never deployed" (live-URL checks skip
|
|
@@ -80,19 +80,6 @@ function describe(records) {
|
|
|
80
80
|
return `${count} deploy record${count === 1 ? '' : 's'}`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
* Record key for a target's instance (multi-instance targets): the primary
|
|
85
|
-
* keeps today's bare target key (zero breaking change — existing records
|
|
86
|
-
* stay valid); any other instance keys `<target>:<id>`, matching the
|
|
87
|
-
* per-target deploy model (each target deploys its own instance).
|
|
88
|
-
* @param {string} target - Target key (web/backend/desktop/extension)
|
|
89
|
-
* @param {string} [instance] - Instance id ('main' or absent = the primary)
|
|
90
|
-
* @returns {string} The deploy-record key
|
|
91
|
-
*/
|
|
92
|
-
function deployKey(target, instance) {
|
|
93
|
-
return instance && instance !== 'main' ? `${target}:${instance}` : target;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
83
|
/**
|
|
97
84
|
* Run fn under a best-effort cross-process lock on the records file, so two
|
|
98
85
|
* targets deploying in parallel can't drop each other's record in the
|
|
@@ -142,15 +129,13 @@ function withStateLock(file, fn) {
|
|
|
142
129
|
*
|
|
143
130
|
* @param {Object} options
|
|
144
131
|
* @param {string} options.dir - Any directory inside the brand (target or root)
|
|
145
|
-
* @param {string} options.target - Target
|
|
146
|
-
* @param {string} [options.instance] - Instance id; non-main instances record
|
|
147
|
-
* under their own `<target>:<id>` key (per-target deploy records)
|
|
132
|
+
* @param {string} options.target - Target name (web, admin, backend, desktop, …)
|
|
148
133
|
* @param {Object} [options.detail] - Extra fields (method, adopted, …)
|
|
149
134
|
* @returns {Object} The written record
|
|
150
135
|
*/
|
|
151
136
|
function recordDeploy(options) {
|
|
152
137
|
const file = stateFile(options.dir);
|
|
153
|
-
const key =
|
|
138
|
+
const key = options.target;
|
|
154
139
|
|
|
155
140
|
return withStateLock(file, () => {
|
|
156
141
|
const state = loadState(file);
|
|
@@ -163,18 +148,17 @@ function recordDeploy(options) {
|
|
|
163
148
|
}
|
|
164
149
|
|
|
165
150
|
/**
|
|
166
|
-
* The recorded deploy for a target
|
|
167
|
-
*
|
|
168
|
-
* site yet).
|
|
151
|
+
* The recorded deploy for a target, null when the brand has never deployed it
|
|
152
|
+
* from this machine (and no manage run has adopted a live site yet).
|
|
169
153
|
*
|
|
170
|
-
* @param {Object} options - { dir, target
|
|
154
|
+
* @param {Object} options - { dir, target }
|
|
171
155
|
* @returns {Object|null}
|
|
172
156
|
*/
|
|
173
157
|
function readDeployRecord(options) {
|
|
174
158
|
const file = stateFile(options.dir);
|
|
175
159
|
// Under the lock because the read is also where a legacy record is adopted
|
|
176
160
|
const state = withStateLock(file, () => loadState(file));
|
|
177
|
-
return state.deploy[
|
|
161
|
+
return state.deploy[options.target] || null;
|
|
178
162
|
}
|
|
179
163
|
|
|
180
|
-
module.exports = { recordDeploy, readDeployRecord
|
|
164
|
+
module.exports = { recordDeploy, readDeployRecord };
|