@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,135 @@
|
|
|
1
|
+
// The DMG hook: artifactBuildCompleted notarizes, staples and PROVES each disk
|
|
2
|
+
// image BEFORE electron-builder uploads it
|
|
3
|
+
// ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
|
|
4
|
+
//
|
|
5
|
+
// The ticket the afterSign hook staples to the .app rides INSIDE the image, so
|
|
6
|
+
// a downloaded .dmg is assessed on its own and an unstapled one is refused on an
|
|
7
|
+
// offline Mac. And the hook has to run per artifact: `afterAllArtifactBuild`
|
|
8
|
+
// fires after every upload is queued, so run 34738410986 published a signed
|
|
9
|
+
// and notarized image that carried no staple. Offline by construction here:
|
|
10
|
+
// `xcrun` and `spctl` are a fake runner injected through the hook's own option,
|
|
11
|
+
// never a global.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const defineCases = require('../../../vendor/devkit/test/define-cases.js');
|
|
17
|
+
|
|
18
|
+
const notarizeArtifacts = require(path.join(__dirname, '..', '..', '..', 'hooks', 'notarize-artifacts.js'));
|
|
19
|
+
|
|
20
|
+
const ACCEPTED = {
|
|
21
|
+
'notarytool submit': ' status: Accepted\n message: Successfully received submission info',
|
|
22
|
+
'stapler staple': 'The staple and validate action worked!',
|
|
23
|
+
'stapler validate': 'The validate action worked!',
|
|
24
|
+
'spctl --assess': '/tmp/App.dmg: accepted\nsource=Notarized Developer ID',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function fakeTools(overrides = {}) {
|
|
28
|
+
const commands = [];
|
|
29
|
+
const answers = { ...ACCEPTED, ...overrides };
|
|
30
|
+
|
|
31
|
+
const run = async (command) => {
|
|
32
|
+
commands.push(command);
|
|
33
|
+
const key = Object.keys(answers).find((needle) => command.includes(needle));
|
|
34
|
+
const answer = key ? answers[key] : '';
|
|
35
|
+
if (answer instanceof Error) throw answer;
|
|
36
|
+
return answer;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return { run, commands };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The credentials the hook needs, restored after each case.
|
|
43
|
+
function withCredentials(body) {
|
|
44
|
+
const prior = {
|
|
45
|
+
APPLE_API_KEY: process.env.APPLE_API_KEY,
|
|
46
|
+
APPLE_API_KEY_ID: process.env.APPLE_API_KEY_ID,
|
|
47
|
+
APPLE_API_ISSUER: process.env.APPLE_API_ISSUER,
|
|
48
|
+
};
|
|
49
|
+
const keyFile = path.join(os.tmpdir(), 'AuthKey_ARTIFACTS_TEST.p8');
|
|
50
|
+
fs.writeFileSync(keyFile, 'fixture-key');
|
|
51
|
+
process.env.APPLE_API_KEY = keyFile;
|
|
52
|
+
process.env.APPLE_API_KEY_ID = 'TESTKEYID1';
|
|
53
|
+
process.env.APPLE_API_ISSUER = 'issuer-uuid';
|
|
54
|
+
|
|
55
|
+
const restore = () => {
|
|
56
|
+
fs.rmSync(keyFile, { force: true });
|
|
57
|
+
for (const [name, value] of Object.entries(prior)) {
|
|
58
|
+
if (value) process.env[name] = value;
|
|
59
|
+
else delete process.env[name];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return Promise.resolve(body(keyFile)).finally(restore);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = defineCases({
|
|
67
|
+
type: 'suite',
|
|
68
|
+
layer: 'build',
|
|
69
|
+
description: 'notarize-artifacts: every DMG is notarized, stapled and proved before its upload (#891)',
|
|
70
|
+
tests: [
|
|
71
|
+
{
|
|
72
|
+
name: 'a .dmg artifact is submitted, stapled, validated and assessed; other artifacts are ignored',
|
|
73
|
+
run: (ctx) => withCredentials(async (keyFile) => {
|
|
74
|
+
const { run, commands } = fakeTools();
|
|
75
|
+
|
|
76
|
+
// electron-builder calls the hook once per artifact, with that
|
|
77
|
+
// artifact's `file`, before it queues the upload.
|
|
78
|
+
for (const file of ['/tmp/App-1.0.0.dmg', '/tmp/App-1.0.0-mac.zip', '/tmp/latest-mac.yml']) {
|
|
79
|
+
await notarizeArtifacts({ file }, { run });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ctx.expect(commands.length).toBe(4);
|
|
83
|
+
ctx.expect(commands[0]).toContain('xcrun notarytool submit "/tmp/App-1.0.0.dmg"');
|
|
84
|
+
ctx.expect(commands[0]).toContain(`--key "${keyFile}" --key-id "TESTKEYID1" --issuer "issuer-uuid" --wait`);
|
|
85
|
+
ctx.expect(commands[1]).toContain('xcrun stapler staple "/tmp/App-1.0.0.dmg"');
|
|
86
|
+
ctx.expect(commands[2]).toContain('xcrun stapler validate "/tmp/App-1.0.0.dmg"');
|
|
87
|
+
ctx.expect(commands[3]).toContain('spctl --assess --type open --context context:primary-signature -vv "/tmp/App-1.0.0.dmg"');
|
|
88
|
+
// The .zip and the update feed are never submitted
|
|
89
|
+
ctx.expect(commands.some((command) => command.includes('.zip') || command.includes('.yml'))).toBe(false);
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'a non-DMG artifact is a no-op: the linux and windows legs never reach a tool',
|
|
94
|
+
run: async (ctx) => {
|
|
95
|
+
const { run, commands } = fakeTools();
|
|
96
|
+
|
|
97
|
+
await notarizeArtifacts({ file: '/tmp/App.AppImage' }, { run });
|
|
98
|
+
await notarizeArtifacts({ file: '/tmp/App.exe' }, { run });
|
|
99
|
+
ctx.expect(commands).toEqual([]);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'a rejected submission throws, so the artifact is never uploaded',
|
|
104
|
+
run: (ctx) => withCredentials(async () => {
|
|
105
|
+
const invalid = fakeTools({ 'notarytool submit': ' status: Invalid\n message: Package Invalid' });
|
|
106
|
+
await ctx.expect(() => notarizeArtifacts({ file: '/tmp/App.dmg' }, { run: invalid.run }))
|
|
107
|
+
.toThrow(/notarization of \/tmp\/App\.dmg was not accepted/);
|
|
108
|
+
// Nothing was stapled onto an artifact Apple refused
|
|
109
|
+
ctx.expect(invalid.commands.length).toBe(1);
|
|
110
|
+
|
|
111
|
+
const refused = fakeTools({ 'spctl --assess': new Error('/tmp/App.dmg: rejected') });
|
|
112
|
+
await ctx.expect(() => notarizeArtifacts({ file: '/tmp/App.dmg' }, { run: refused.run }))
|
|
113
|
+
.toThrow(/Gatekeeper refused/);
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'a DMG with no APPLE_* credentials throws instead of shipping unnotarized',
|
|
118
|
+
run: async (ctx) => {
|
|
119
|
+
const prior = { key: process.env.APPLE_API_KEY, id: process.env.APPLE_API_KEY_ID, issuer: process.env.APPLE_API_ISSUER };
|
|
120
|
+
delete process.env.APPLE_API_KEY;
|
|
121
|
+
delete process.env.APPLE_API_KEY_ID;
|
|
122
|
+
delete process.env.APPLE_API_ISSUER;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const { run } = fakeTools();
|
|
126
|
+
await ctx.expect(() => notarizeArtifacts({ file: '/tmp/App.dmg' }, { run })).toThrow(/APPLE_API_KEY/);
|
|
127
|
+
} finally {
|
|
128
|
+
if (prior.key) process.env.APPLE_API_KEY = prior.key;
|
|
129
|
+
if (prior.id) process.env.APPLE_API_KEY_ID = prior.id;
|
|
130
|
+
if (prior.issuer) process.env.APPLE_API_ISSUER = prior.issuer;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The notarization tools' command runner
|
|
2
|
+
// ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
|
|
3
|
+
//
|
|
4
|
+
// notarytool, stapler and spctl write their verdict to the console and exit
|
|
5
|
+
// non-zero when they refuse, so the runner must hand back what they SAID no
|
|
6
|
+
// matter how they exited. Run 34735658588's DMG assessment failed with only
|
|
7
|
+
// `Command failed with exit code 3`: the verdict itself was lost.
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const defineCases = require('../../../vendor/devkit/test/define-cases.js');
|
|
11
|
+
|
|
12
|
+
const { report, toolRunner } = require(path.join(__dirname, '..', '..', '..', 'hooks', 'lib', 'notarize-tools.js'));
|
|
13
|
+
|
|
14
|
+
module.exports = defineCases({
|
|
15
|
+
type: 'suite',
|
|
16
|
+
layer: 'build',
|
|
17
|
+
description: 'notarize-tools: the runner reports a refusing tool\'s own words (#891)',
|
|
18
|
+
tests: [
|
|
19
|
+
{
|
|
20
|
+
name: 'a command that exits non-zero still reports its stdout and stderr',
|
|
21
|
+
run: async (ctx) => {
|
|
22
|
+
const output = await report(toolRunner(), 'echo "App.dmg: rejected"; echo "source=Insufficient Context" 1>&2; exit 3');
|
|
23
|
+
|
|
24
|
+
ctx.expect(output).toContain('App.dmg: rejected');
|
|
25
|
+
ctx.expect(output).toContain('source=Insufficient Context');
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'a command that exits zero reports its output the same way',
|
|
30
|
+
run: async (ctx) => {
|
|
31
|
+
const output = await report(toolRunner(), 'echo "App.dmg: accepted"; echo "source=Notarized Developer ID" 1>&2');
|
|
32
|
+
|
|
33
|
+
ctx.expect(output).toContain('App.dmg: accepted');
|
|
34
|
+
ctx.expect(output).toContain('source=Notarized Developer ID');
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// The notarize hook's Developer ID gate
|
|
2
|
+
// ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)).
|
|
3
|
+
//
|
|
4
|
+
// electron-builder SKIPS mac code signing when it finds no identity ("cannot find
|
|
5
|
+
// valid Developer ID Application identity ... 0 identities found") and ships the
|
|
6
|
+
// ad-hoc signature the linker left. The hook then notarized it anyway, because
|
|
7
|
+
// APPLE_API_KEY was set, and notarytool rejected the submission: "Failed to
|
|
8
|
+
// codesign your application with code: 1" ended the whole mac leg of a build that
|
|
9
|
+
// had otherwise succeeded.
|
|
10
|
+
//
|
|
11
|
+
// The seam is codesign's OWN report: a pure predicate over its text, plus an
|
|
12
|
+
// injectable exec for reading it. Both fixtures below are the verbatim output of
|
|
13
|
+
// run 34584322778 and of a signed build.
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const defineCases = require('../../../vendor/devkit/test/define-cases.js');
|
|
17
|
+
|
|
18
|
+
const notarizeHook = require(path.join(__dirname, '..', '..', '..', 'hooks', 'notarize.js'));
|
|
19
|
+
const { isDeveloperIdSigned, readCodesign } = notarizeHook;
|
|
20
|
+
const { stapleAndProve } = require(path.join(__dirname, '..', '..', '..', 'hooks', 'lib', 'notarize-tools.js'));
|
|
21
|
+
|
|
22
|
+
// What the real tools print when they are happy. A fake `run` answers by
|
|
23
|
+
// command, so the SEQUENCE and the refusal are both provable offline.
|
|
24
|
+
const TOOL_OUTPUT = {
|
|
25
|
+
'stapler staple': 'Processing: /tmp/App.app\nThe staple and validate action worked!',
|
|
26
|
+
'stapler validate': 'Processing: /tmp/App.app\nThe validate action worked!',
|
|
27
|
+
'spctl --assess': '/tmp/App.app: accepted\nsource=Notarized Developer ID',
|
|
28
|
+
'notarytool submit': ' status: Accepted\n message: Successfully received submission info',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function fakeTools(overrides = {}) {
|
|
32
|
+
const commands = [];
|
|
33
|
+
const answers = { ...TOOL_OUTPUT, ...overrides };
|
|
34
|
+
|
|
35
|
+
const run = async (command) => {
|
|
36
|
+
commands.push(command);
|
|
37
|
+
const key = Object.keys(answers).find((needle) => command.includes(needle));
|
|
38
|
+
const answer = key ? answers[key] : '';
|
|
39
|
+
if (answer instanceof Error) throw answer;
|
|
40
|
+
return answer;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { run, commands };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// What the runner's own `codesign -dv --verbose=2` printed for the unsigned app.
|
|
47
|
+
const ADHOC = `Executable=/Users/runner/work/playground-omega/targets/desktop/release/mac-universal/OMEGA Playground.app/Contents/MacOS/OMEGA Playground
|
|
48
|
+
Identifier=Electron
|
|
49
|
+
Format=app bundle with Mach-O universal (x86_64 arm64)
|
|
50
|
+
CodeDirectory v=20400 size=392 flags=0x20002(adhoc,linker-signed) hashes=9+0 location=embedded
|
|
51
|
+
Signature=adhoc
|
|
52
|
+
Info.plist=not bound
|
|
53
|
+
TeamIdentifier=not set
|
|
54
|
+
Sealed Resources=none`;
|
|
55
|
+
|
|
56
|
+
// The same report for a build that DID find the identity.
|
|
57
|
+
const SIGNED = `Executable=/Users/runner/work/app/release/mac-universal/App.app/Contents/MacOS/App
|
|
58
|
+
Identifier=dev.omegajs.app
|
|
59
|
+
Format=app bundle with Mach-O universal (x86_64 arm64)
|
|
60
|
+
CodeDirectory v=20500 size=1234 flags=0x10000(runtime) hashes=30+7 location=embedded
|
|
61
|
+
Signature size=9000
|
|
62
|
+
Authority=Developer ID Application: ITW Creative Works (ABCDE12345)
|
|
63
|
+
Authority=Developer ID Certification Authority
|
|
64
|
+
Authority=Apple Root CA
|
|
65
|
+
TeamIdentifier=ABCDE12345
|
|
66
|
+
Sealed Resources version=2 rules=13 files=40`;
|
|
67
|
+
|
|
68
|
+
// An app with no signature at all: codesign exits NON-ZERO and says so.
|
|
69
|
+
const UNSIGNED_ERROR = 'OMEGA Playground.app: code object is not signed at all';
|
|
70
|
+
|
|
71
|
+
module.exports = defineCases({
|
|
72
|
+
type: 'suite',
|
|
73
|
+
layer: 'build',
|
|
74
|
+
description: 'notarize: only a Developer ID signed app is submitted (#872), and the staple is PROVED (#891)',
|
|
75
|
+
tests: [
|
|
76
|
+
{
|
|
77
|
+
name: 'an ad-hoc signature is not a Developer ID signature',
|
|
78
|
+
run: (ctx) => {
|
|
79
|
+
ctx.expect(isDeveloperIdSigned(ADHOC)).toBe(false);
|
|
80
|
+
ctx.expect(isDeveloperIdSigned(UNSIGNED_ERROR)).toBe(false);
|
|
81
|
+
ctx.expect(isDeveloperIdSigned('')).toBe(false);
|
|
82
|
+
ctx.expect(isDeveloperIdSigned(undefined)).toBe(false);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'a Developer ID Application authority is',
|
|
87
|
+
run: (ctx) => {
|
|
88
|
+
ctx.expect(isDeveloperIdSigned(SIGNED)).toBe(true);
|
|
89
|
+
|
|
90
|
+
// A signature from some OTHER authority cannot be notarized either.
|
|
91
|
+
ctx.expect(isDeveloperIdSigned(SIGNED.replace('Authority=Developer ID Application:', 'Authority=Apple Development:'))).toBe(false);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'the report is read with stderr folded in, and a failing codesign is the answer',
|
|
96
|
+
run: async (ctx) => {
|
|
97
|
+
const commands = [];
|
|
98
|
+
const report = await readCodesign('/tmp/App.app', (cmd) => {
|
|
99
|
+
commands.push(cmd);
|
|
100
|
+
return Promise.resolve(SIGNED);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// codesign writes its report to STDERR and exits 0, so a plain stdout
|
|
104
|
+
// read comes back EMPTY and every app would look unsigned.
|
|
105
|
+
ctx.expect(commands[0]).toContain('2>&1');
|
|
106
|
+
ctx.expect(commands[0]).toContain('/tmp/App.app');
|
|
107
|
+
ctx.expect(isDeveloperIdSigned(report)).toBe(true);
|
|
108
|
+
|
|
109
|
+
// An unsigned app exits non-zero; the report rides in the rejection.
|
|
110
|
+
const failed = await readCodesign('/tmp/App.app', () => Promise.reject(new Error(UNSIGNED_ERROR)));
|
|
111
|
+
ctx.expect(failed).toContain('not signed at all');
|
|
112
|
+
ctx.expect(isDeveloperIdSigned(failed)).toBe(false);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// ── The staple and its PROOF (#891) ─────────────────────────────────────
|
|
116
|
+
// Proof run one published an app no other Mac would open. Notarizing is
|
|
117
|
+
// half the job: the ticket has to be stapled, and the staple has to be
|
|
118
|
+
// read back, or an offline Mac still refuses.
|
|
119
|
+
{
|
|
120
|
+
name: 'stapling runs staple → validate → spctl, in that order, on the .app',
|
|
121
|
+
run: async (ctx) => {
|
|
122
|
+
const { run, commands } = fakeTools();
|
|
123
|
+
|
|
124
|
+
await stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run });
|
|
125
|
+
|
|
126
|
+
ctx.expect(commands.length).toBe(3);
|
|
127
|
+
ctx.expect(commands[0]).toContain('xcrun stapler staple "/tmp/App.app"');
|
|
128
|
+
ctx.expect(commands[1]).toContain('xcrun stapler validate "/tmp/App.app"');
|
|
129
|
+
ctx.expect(commands[2]).toContain('spctl --assess --type execute -vv "/tmp/App.app"');
|
|
130
|
+
// The verdict these tools write to stderr is the RUNNER's to capture
|
|
131
|
+
// (notarize-tools.test.js), never a shell fold on the command.
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'a DMG is assessed as an OPENED image against its own signature',
|
|
136
|
+
run: async (ctx) => {
|
|
137
|
+
const { run, commands } = fakeTools({ 'spctl --assess': '/tmp/App.dmg: accepted\nsource=Notarized Developer ID' });
|
|
138
|
+
|
|
139
|
+
await stapleAndProve({ filePath: '/tmp/App.dmg', kind: 'dmg', run });
|
|
140
|
+
|
|
141
|
+
ctx.expect(commands[2]).toContain('spctl --assess --type open --context context:primary-signature -vv "/tmp/App.dmg"');
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'a REJECTED assessment throws: the release step never runs',
|
|
146
|
+
run: async (ctx) => {
|
|
147
|
+
// spctl exits non-zero and says so on stderr
|
|
148
|
+
const rejected = fakeTools({ 'spctl --assess': new Error('/tmp/App.app: rejected\nsource=no usable signature') });
|
|
149
|
+
await ctx.expect(() => stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run: rejected.run })).toThrow(/Gatekeeper refused/);
|
|
150
|
+
|
|
151
|
+
// ...and so does a staple that never took, or a ticket that does not read back
|
|
152
|
+
const unstapled = fakeTools({ 'stapler staple': new Error('The staple and validate action failed! Error 65.') });
|
|
153
|
+
await ctx.expect(() => stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run: unstapled.run })).toThrow(/could not staple/);
|
|
154
|
+
|
|
155
|
+
const unreadable = fakeTools({ 'stapler validate': new Error('The validate action failed! Error 65.') });
|
|
156
|
+
await ctx.expect(() => stapleAndProve({ filePath: '/tmp/App.app', kind: 'app', run: unreadable.run })).toThrow(/carries no valid notarization ticket/);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
// ── The hook's own refusals (#891) ──────────────────────────────────────
|
|
160
|
+
// Both of these used to be `logger.warn` + `return`, and BOTH of them were
|
|
161
|
+
// hit on the run that shipped an unsigned app.
|
|
162
|
+
{
|
|
163
|
+
name: 'a mac build with no APPLE_* credentials THROWS instead of skipping',
|
|
164
|
+
run: async (ctx) => {
|
|
165
|
+
const prior = { key: process.env.APPLE_API_KEY, id: process.env.APPLE_API_KEY_ID, issuer: process.env.APPLE_API_ISSUER };
|
|
166
|
+
delete process.env.APPLE_API_KEY;
|
|
167
|
+
delete process.env.APPLE_API_KEY_ID;
|
|
168
|
+
delete process.env.APPLE_API_ISSUER;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await ctx.expect(() => notarizeHook({ electronPlatformName: 'darwin', appOutDir: '/tmp' })).toThrow(/APPLE_API_KEY/);
|
|
172
|
+
// A non-mac leg is still the ONE legitimate skip
|
|
173
|
+
ctx.expect(await notarizeHook({ electronPlatformName: 'win32', appOutDir: '/tmp' })).toBe(undefined);
|
|
174
|
+
} finally {
|
|
175
|
+
if (prior.key) process.env.APPLE_API_KEY = prior.key;
|
|
176
|
+
if (prior.id) process.env.APPLE_API_KEY_ID = prior.id;
|
|
177
|
+
if (prior.issuer) process.env.APPLE_API_ISSUER = prior.issuer;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'an ad-hoc signed .app THROWS instead of publishing unnotarized',
|
|
183
|
+
run: async (ctx) => {
|
|
184
|
+
const prior = { key: process.env.APPLE_API_KEY, id: process.env.APPLE_API_KEY_ID, issuer: process.env.APPLE_API_ISSUER };
|
|
185
|
+
const keyFile = path.join(require('os').tmpdir(), `AuthKey_NOTARIZE_TEST.p8`);
|
|
186
|
+
require('fs').writeFileSync(keyFile, 'fixture-key');
|
|
187
|
+
process.env.APPLE_API_KEY = keyFile;
|
|
188
|
+
process.env.APPLE_API_KEY_ID = 'TESTKEYID1';
|
|
189
|
+
process.env.APPLE_API_ISSUER = 'issuer-uuid';
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const { run } = fakeTools({ 'codesign -dv': ADHOC });
|
|
193
|
+
await ctx.expect(() => notarizeHook(
|
|
194
|
+
{ electronPlatformName: 'darwin', appOutDir: '/tmp', packager: { appInfo: { productFilename: 'App' } } },
|
|
195
|
+
{ run },
|
|
196
|
+
)).toThrow(/is NOT Developer ID signed/);
|
|
197
|
+
} finally {
|
|
198
|
+
require('fs').rmSync(keyFile, { force: true });
|
|
199
|
+
for (const [name, value] of [['APPLE_API_KEY', prior.key], ['APPLE_API_KEY_ID', prior.id], ['APPLE_API_ISSUER', prior.issuer]]) {
|
|
200
|
+
if (value) process.env[name] = value;
|
|
201
|
+
else delete process.env[name];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
@@ -8,7 +8,7 @@ const os = require('os');
|
|
|
8
8
|
const defineCases = require('../../../vendor/devkit/test/define-cases.js');
|
|
9
9
|
|
|
10
10
|
// Helper: stage a consumer dir with config/omega.json5 containing the given
|
|
11
|
-
// platforms.
|
|
11
|
+
// platforms.windows.signing.strategy (under targets.desktop in the raw file). Returns the
|
|
12
12
|
// abs path to the temp dir; caller is responsible for chdir'ing into it and cleaning up.
|
|
13
13
|
function stageStrategyConfig({ strategy, cloudProvider }) {
|
|
14
14
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-sign-'));
|
|
@@ -17,7 +17,7 @@ function stageStrategyConfig({ strategy, cloudProvider }) {
|
|
|
17
17
|
if (cloudProvider) signing.cloud = { provider: cloudProvider };
|
|
18
18
|
fs.writeFileSync(
|
|
19
19
|
path.join(tmp, 'config', 'omega.json5'),
|
|
20
|
-
JSON.stringify({ targets: { desktop: { platforms: {
|
|
20
|
+
JSON.stringify({ targets: { desktop: { type: 'desktop', platforms: { windows: { signing } } } } }),
|
|
21
21
|
);
|
|
22
22
|
return tmp;
|
|
23
23
|
}
|
|
@@ -39,74 +39,51 @@ module.exports = defineCases({
|
|
|
39
39
|
layer: 'build',
|
|
40
40
|
description: 'release pipeline — package / release / sign-windows / notarize hook',
|
|
41
41
|
tests: [
|
|
42
|
-
{
|
|
43
|
-
name: 'release: the dispatch address is the CONFIG\'s brand repo and the COMPOSED workflow name (#799)',
|
|
44
|
-
run: (ctx) => {
|
|
45
|
-
const { dispatchTarget } = require(path.join(__dirname, '..', '..', '..', 'commands', 'release.js'));
|
|
46
|
-
const { brandRoot, targetDir } = stageBrand({ brand: { id: 'acme' }, repo: { providers: { github: { org: 'Acme-Org' } } } });
|
|
47
42
|
|
|
48
|
-
try {
|
|
49
|
-
// A target nested in a brand (and the brand nested in another repo, as
|
|
50
|
-
// the playground is here) dispatches on the repo its CONFIG names: the
|
|
51
|
-
// enclosing git remote is somebody else's repo entirely.
|
|
52
|
-
// The name is the derived `<brand.id>-omega` default (#809): the config
|
|
53
|
-
// declares an org and no repo of its own.
|
|
54
|
-
ctx.expect(dispatchTarget({
|
|
55
|
-
projectRoot: targetDir,
|
|
56
|
-
config: { brand: { id: 'acme' }, repo: { providers: { github: { org: 'Acme-Org' } } } },
|
|
57
|
-
})).toEqual({ owner: 'Acme-Org', repo: 'acme-omega', workflow: 'desktop-build.yml' });
|
|
58
|
-
} finally {
|
|
59
|
-
fs.rmSync(brandRoot, { recursive: true, force: true });
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
43
|
{
|
|
64
|
-
name: '
|
|
65
|
-
run: (ctx) => {
|
|
66
|
-
const {
|
|
67
|
-
const
|
|
44
|
+
name: 'deploy-precheck: provisions <org>/<brand.id>-releases PUBLIC through the devkit ensureRepo (#799, #883)',
|
|
45
|
+
run: async (ctx) => {
|
|
46
|
+
const { provisionReleaseRepos } = require(path.join(__dirname, '..', '..', '..', 'commands', 'lib', 'deploy-precheck.js'));
|
|
47
|
+
const calls = [];
|
|
48
|
+
const lines = [];
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
await provisionReleaseRepos({
|
|
51
|
+
log: (line) => lines.push(line),
|
|
52
|
+
warn: (line) => lines.push(line),
|
|
53
|
+
config: { brand: { id: 'omega-playground' }, repo: { provider: 'github', org: 'Omega-JS-Stack' }, releases: {} },
|
|
54
|
+
// The ONE github boundary (#883): `gh` with an argv array, so the
|
|
55
|
+
// create is readable here exactly as it runs.
|
|
56
|
+
execFn: (file, args) => {
|
|
57
|
+
calls.push({ file, args });
|
|
58
|
+
if (args[0] === 'api') throw new Error('gh api failed: 404 Not Found');
|
|
59
|
+
return '';
|
|
60
|
+
},
|
|
61
|
+
});
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
63
|
+
const create = calls.find((call) => call.args[0] === 'repo');
|
|
64
|
+
ctx.expect(create.file).toBe('gh');
|
|
65
|
+
ctx.expect(create.args.slice(0, 4)).toEqual(['repo', 'create', 'Omega-JS-Stack/omega-playground-releases', '--public']);
|
|
66
|
+
// A release needs a tag and a tag needs a commit: this is the one repo
|
|
67
|
+
// that is created WITH one.
|
|
68
|
+
ctx.expect(create.args).toContain('--add-readme');
|
|
69
|
+
ctx.expect(lines.join('\n')).toContain('Omega-JS-Stack/omega-playground-releases');
|
|
82
70
|
},
|
|
83
71
|
},
|
|
72
|
+
|
|
84
73
|
{
|
|
85
|
-
name: 'deploy-precheck:
|
|
74
|
+
name: 'deploy-precheck: a config that names no org warns and creates nothing (#883)',
|
|
86
75
|
run: async (ctx) => {
|
|
87
76
|
const { provisionReleaseRepos } = require(path.join(__dirname, '..', '..', '..', 'commands', 'lib', 'deploy-precheck.js'));
|
|
88
|
-
const created = [];
|
|
89
|
-
const octokit = {
|
|
90
|
-
rest: {
|
|
91
|
-
repos: {
|
|
92
|
-
get: async () => { const error = new Error('Not Found'); error.status = 404; throw error; },
|
|
93
|
-
createInOrg: async (args) => created.push(`${args.org}/${args.name}`),
|
|
94
|
-
createForAuthenticatedUser: async (args) => created.push(`me/${args.name}`),
|
|
95
|
-
},
|
|
96
|
-
users: { getAuthenticated: async () => ({ data: { login: 'somebody-else' } }) },
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
77
|
const lines = [];
|
|
100
78
|
|
|
101
79
|
await provisionReleaseRepos({
|
|
102
80
|
log: (line) => lines.push(line),
|
|
103
81
|
warn: (line) => lines.push(line),
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
config: { brand: { id: 'acme' }, releases: {} },
|
|
83
|
+
execFn: () => { throw new Error('gh must not run'); },
|
|
106
84
|
});
|
|
107
85
|
|
|
108
|
-
ctx.expect(
|
|
109
|
-
ctx.expect(lines.join('\n')).toContain('Omega-JS-Stack/omega-playground-releases');
|
|
86
|
+
ctx.expect(lines.join('\n')).toContain('repo.org');
|
|
110
87
|
},
|
|
111
88
|
},
|
|
112
89
|
{
|
|
@@ -115,8 +92,8 @@ module.exports = defineCases({
|
|
|
115
92
|
const finalizeRelease = require(path.join(__dirname, '..', '..', '..', 'commands', 'finalize-release.js'));
|
|
116
93
|
const { brandRoot, targetDir } = stageBrand({
|
|
117
94
|
brand: { id: 'acme' },
|
|
118
|
-
repo: {
|
|
119
|
-
targets: { desktop: { releases: {} } },
|
|
95
|
+
repo: { provider: 'github', org: 'Acme-Org' },
|
|
96
|
+
targets: { desktop: { type: 'desktop', releases: {} } },
|
|
120
97
|
});
|
|
121
98
|
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify({ name: 'acme-desktop', version: '1.2.3' }));
|
|
122
99
|
|
|
@@ -333,7 +310,9 @@ module.exports = defineCases({
|
|
|
333
310
|
},
|
|
334
311
|
},
|
|
335
312
|
{
|
|
336
|
-
|
|
313
|
+
// Flipped by #891: a mac build that reaches this hook is one this
|
|
314
|
+
// framework ships, so credentials it cannot find stop the build.
|
|
315
|
+
name: 'notarize hook: THROWS when the API key env vars are missing',
|
|
337
316
|
run: async (ctx) => {
|
|
338
317
|
const notarize = require(path.join(__dirname, '..', '..', '..', 'hooks', 'notarize.js'));
|
|
339
318
|
// Snapshot + clear env.
|
|
@@ -347,12 +326,11 @@ module.exports = defineCases({
|
|
|
347
326
|
delete process.env.APPLE_API_ISSUER;
|
|
348
327
|
|
|
349
328
|
try {
|
|
350
|
-
|
|
329
|
+
await ctx.expect(() => notarize({
|
|
351
330
|
electronPlatformName: 'darwin',
|
|
352
331
|
appOutDir: '/tmp',
|
|
353
332
|
packager: { appInfo: { productFilename: 'test' } },
|
|
354
|
-
});
|
|
355
|
-
ctx.expect(result).toBeUndefined();
|
|
333
|
+
})).toThrow(/APPLE_API_KEY, APPLE_API_KEY_ID and APPLE_API_ISSUER are required/);
|
|
356
334
|
} finally {
|
|
357
335
|
for (const [k, v] of Object.entries(snapshot)) {
|
|
358
336
|
if (v !== undefined) process.env[k] = v;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The release task's verdict on a build that uploaded NOTHING
|
|
2
|
+
// ([#891](https://github.com/Omega-JS-Stack/omega/issues/891)).
|
|
3
|
+
//
|
|
4
|
+
// electron-builder's `build()` promise resolves with the LOCAL artifact paths
|
|
5
|
+
// whether or not a byte reached the publish provider, so every leg of run
|
|
6
|
+
// 34729947722 printed `Released 4 artifact(s)` while electron-publish had
|
|
7
|
+
// logged `skipped publishing` for each of them
|
|
8
|
+
// (`reason=existing release published more than 2 hours ago tag=v0.0.1`). A
|
|
9
|
+
// release that uploaded nothing is a failed release, and the only report of the
|
|
10
|
+
// skip is electron-builder's own logger, so the task reads it there.
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const defineCases = require('../../../vendor/devkit/test/define-cases.js');
|
|
14
|
+
|
|
15
|
+
const RELEASE_TASK = path.join(__dirname, '..', '..', '..', 'gulp', 'tasks', 'release.js');
|
|
16
|
+
|
|
17
|
+
// The fields electron-publish's gitHubPublisher carries on both warnings.
|
|
18
|
+
const PUBLISH_FIELDS = { tag: 'v0.0.1', reason: 'existing release published more than 2 hours ago' };
|
|
19
|
+
|
|
20
|
+
// A stand-in for builder-util's `log`: a Logger whose `warn(fields, message)`
|
|
21
|
+
// takes the two shapes the real one does, recording what it was handed.
|
|
22
|
+
function fakeBuilderLog() {
|
|
23
|
+
const seen = [];
|
|
24
|
+
return {
|
|
25
|
+
seen,
|
|
26
|
+
warn(messageOrFields, message) {
|
|
27
|
+
seen.push(message === undefined ? messageOrFields : message);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = defineCases({
|
|
33
|
+
type: 'suite',
|
|
34
|
+
layer: 'build',
|
|
35
|
+
description: 'release task (#891): a skipped upload is a failed release, never a "Released N artifact(s)" line',
|
|
36
|
+
tests: [
|
|
37
|
+
{
|
|
38
|
+
name: 'every skipped publish is recorded off the build logger, with its reason and tag',
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
const { recordSkippedPublishes } = require(RELEASE_TASK);
|
|
41
|
+
const log = fakeBuilderLog();
|
|
42
|
+
const original = log.warn;
|
|
43
|
+
|
|
44
|
+
const { result, skips } = await recordSkippedPublishes(log, async () => {
|
|
45
|
+
log.warn(PUBLISH_FIELDS, 'GitHub release not created');
|
|
46
|
+
log.warn({ file: 'App-0.0.1.dmg', ...PUBLISH_FIELDS }, 'skipped publishing');
|
|
47
|
+
log.warn({ file: 'App-0.0.1-mac.zip', ...PUBLISH_FIELDS }, 'skipped publishing');
|
|
48
|
+
log.warn('cannot compute hash of the pending file');
|
|
49
|
+
return ['/tmp/release/App-0.0.1.dmg', '/tmp/release/App-0.0.1-mac.zip'];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
ctx.expect(result.length).toBe(2);
|
|
53
|
+
ctx.expect(skips.length).toBe(3);
|
|
54
|
+
ctx.expect(skips.map((skip) => skip.file).filter(Boolean)).toEqual(['App-0.0.1.dmg', 'App-0.0.1-mac.zip']);
|
|
55
|
+
ctx.expect(skips[0].reason).toBe(PUBLISH_FIELDS.reason);
|
|
56
|
+
ctx.expect(skips[0].tag).toBe('v0.0.1');
|
|
57
|
+
|
|
58
|
+
// The build's own logging still reaches the logger, and the wrapper is
|
|
59
|
+
// gone the moment the build settles.
|
|
60
|
+
ctx.expect(log.seen.length).toBe(4);
|
|
61
|
+
ctx.expect(log.warn).toBe(original);
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'the wrapper restores the logger even when the build throws',
|
|
66
|
+
run: async (ctx) => {
|
|
67
|
+
const { recordSkippedPublishes } = require(RELEASE_TASK);
|
|
68
|
+
const log = fakeBuilderLog();
|
|
69
|
+
const original = log.warn;
|
|
70
|
+
|
|
71
|
+
let thrown = null;
|
|
72
|
+
try {
|
|
73
|
+
await recordSkippedPublishes(log, async () => { throw new Error('electron-builder exploded'); });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
thrown = error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ctx.expect(thrown.message).toBe('electron-builder exploded');
|
|
79
|
+
ctx.expect(log.warn).toBe(original);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'a recorded skip fails the release naming the reason, the tag and the fix',
|
|
84
|
+
run: (ctx) => {
|
|
85
|
+
const { skippedUploadError } = require(RELEASE_TASK);
|
|
86
|
+
const skips = [
|
|
87
|
+
{ message: 'GitHub release not created', ...PUBLISH_FIELDS },
|
|
88
|
+
{ message: 'skipped publishing', file: 'App-0.0.1.dmg', ...PUBLISH_FIELDS },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const error = skippedUploadError(skips);
|
|
92
|
+
ctx.expect(error instanceof Error).toBe(true);
|
|
93
|
+
ctx.expect(error.message).toContain('existing release published more than 2 hours ago');
|
|
94
|
+
ctx.expect(error.message).toContain('v0.0.1');
|
|
95
|
+
ctx.expect(error.message).toContain('App-0.0.1.dmg');
|
|
96
|
+
ctx.expect(error.message).toContain('Bump the version in package.json');
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'a build that skipped nothing is the release it always was',
|
|
101
|
+
run: (ctx) => {
|
|
102
|
+
const { skippedUploadError } = require(RELEASE_TASK);
|
|
103
|
+
ctx.expect(skippedUploadError([])).toBeNull();
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|