From daf7b7d8fae838f34b6440c8b03dcdeed7c64b20 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 19 Aug 2021 01:36:52 -0700 Subject: [PATCH 1/5] feat: Adding Keygen as an official publisher/updater for electron-builder #6167 --- package.json | 6 +- packages/app-builder-lib/scheme.json | 365 ++++++++++++++---- .../src/publish/BintrayPublisher.ts | 16 +- .../src/publish/KeygenPublisher.ts | 116 ++++++ .../src/publish/PublishManager.ts | 17 + packages/app-builder-lib/src/version.ts | 2 +- packages/app-builder-lib/tsconfig-scheme.json | 19 + packages/app-builder-lib/tsconfig.json | 18 +- .../builder-util-runtime/src/httpExecutor.ts | 67 +++- packages/builder-util-runtime/src/index.ts | 1 + .../src/publishOptions.ts | 37 +- packages/builder-util/src/nodeHttpExecutor.ts | 2 +- packages/electron-publish/src/publisher.ts | 2 +- packages/electron-updater/src/AppUpdater.ts | 9 + packages/electron-updater/src/MacUpdater.ts | 2 +- .../src/electronHttpExecutor.ts | 4 +- .../electron-updater/src/providerFactory.ts | 5 + .../src/providers/KeygenProvider.ts | 53 +++ .../src/providers/Provider.ts | 7 +- pnpm-lock.yaml | 42 +- scripts/update-package-version-export.js | 4 +- test/jestSetup.js | 5 +- test/snapshots/PublishManagerTest.js.snap | 42 +- .../configurationValidationTest.js.snap | 14 + .../snapshots/updater/nsisUpdaterTest.js.snap | 24 ++ test/src/ArtifactPublisherTest.ts | 60 +-- test/src/PublishManagerTest.ts | 42 +- test/src/configurationValidationTest.ts | 6 +- test/src/helpers/updaterTestUtil.ts | 19 +- test/src/updater/nsisUpdaterTest.ts | 13 +- test/typings/jest-ex.d.ts | 2 + 31 files changed, 809 insertions(+), 212 deletions(-) create mode 100644 packages/app-builder-lib/src/publish/KeygenPublisher.ts create mode 100644 packages/app-builder-lib/tsconfig-scheme.json create mode 100644 packages/electron-updater/src/providers/KeygenProvider.ts diff --git a/package.json b/package.json index e9e6b2c818..90c9af06db 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "pretest": "pnpm compile && pnpm lint && pnpm lint-deps", "prettier": "prettier 'packages/**/*.{ts, js}' 'test/src/**/*.ts' --write", "///": "Please see https://github.com/electron-userland/electron-builder/blob/master/CONTRIBUTING.md#run-test-using-cli how to run particular test instead full (and very slow) run", - "test": "node ./test/out/helpers/runTests.js skipArtifactPublisher ALL_TESTS=isCi", + "test": "node ./test/out/helpers/runTests.js skipArtifactPublisher", "test-all": "pnpm pretest && node ./test/out/helpers/runTests.js", "test-linux": "docker run --rm -ti -e UPDATE_SNAPSHOT=${UPDATE_SNAPSHOT:-false} -e TEST_FILES=\"${TEST_FILES:-HoistedNodeModuleTest}\" -v $(pwd):/project -v $(pwd)-node-modules:/project/node_modules -v $HOME/Library/Caches/electron:/root/.cache/electron -v $HOME/Library/Caches/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c \"pnpm install && pnpm node ./test/out/helpers/runTests.js\"", "test-update": "UPDATE_SNAPSHOT=true pnpm test-all", @@ -25,7 +25,7 @@ "generate-changeset": "pnpm changeset", "ci:version": "pnpm changelog && changeset version && node scripts/update-package-version-export.js && git add .", "ci:publish": "pnpm compile && pnpm publish -r", - "schema": "typescript-json-schema packages/app-builder-lib/tsconfig.json Configuration --out packages/app-builder-lib/scheme.json --noExtraProps --useTypeOfKeyword --strictNullChecks --required && node ./scripts/fix-schema.js", + "schema": "typescript-json-schema packages/app-builder-lib/tsconfig-scheme.json Configuration --out packages/app-builder-lib/scheme.json --noExtraProps --useTypeOfKeyword --strictNullChecks --required && node ./scripts/fix-schema.js", "jsdoc": "ts2jsdoc packages/builder-util-runtime packages/builder-util packages/app-builder-lib packages/electron-builder packages/electron-publish", "jsdoc2md": "node scripts/jsdoc2md.js", "/////": "git clone --single-branch -b docs git@github.com:electron-userland/electron-builder.git docs", @@ -63,7 +63,7 @@ "source-map-support": "0.5.19", "ts-jsdoc": "3.2.2", "typescript": "4.3.5", - "typescript-json-schema": "0.50.1", + "typescript-json-schema": "^0.50.1", "v8-compile-cache": "2.3.0" }, "engines": { diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index bd2bcd5890..92f5a6ac45 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -84,6 +84,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -108,6 +111,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -234,6 +240,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -258,6 +267,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -420,12 +432,45 @@ ], "type": "object" }, + "CustomNsisBinary": { + "additionalProperties": false, + "properties": { + "checksum": { + "default": "o+YZsXHp8LNihhuk7JsCDhdIgx0MKKK+1b3sGD+4zX5djZULe4/4QMcAsfQ+0r+a8FnwBt7BVBHkIkJHjKQ0sg==", + "type": [ + "null", + "string" + ] + }, + "url": { + "default": "https://github.com/electron-userland/electron-builder-binaries/releases/download", + "type": [ + "null", + "string" + ] + }, + "version": { + "default": "3.0.4.2", + "type": [ + "null", + "string" + ] + } + }, + "required": [ + "url" + ], + "type": "object" + }, "CustomPublishOptions": { "additionalProperties": {}, "properties": { "provider": { - "$ref": "#/definitions/PublishProvider", - "description": "The provider." + "description": "The provider. Must be `custom`.", + "enum": [ + "custom" + ], + "type": "string" }, "publishAutoUpdate": { "default": true, @@ -449,6 +494,10 @@ "$ref": "#/definitions/OutgoingHttpHeaders", "description": "Any custom request headers" }, + "updateProvider": { + "description": "The Provider to provide UpdateInfo regarding available updates. Required\nto use custom providers with electron-updater.", + "typeof": "function" + }, "updaterCacheDirName": { "type": [ "null", @@ -623,6 +672,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -647,6 +699,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -807,6 +862,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -831,6 +889,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -899,11 +960,12 @@ }, "ElectronBrandingOptions": { "additionalProperties": false, + "description": "Electron distributables branding options.", "properties": { - "projectName": { + "productName": { "type": "string" }, - "productName": { + "projectName": { "type": "string" } }, @@ -1179,6 +1241,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -1203,6 +1268,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -1445,6 +1513,76 @@ ], "type": "object" }, + "KeygenOptions": { + "additionalProperties": false, + "description": "Keygen options.\nhttps://keygen.sh/\nDefine `KEYGEN_TOKEN` environment variable.", + "properties": { + "account": { + "description": "Keygen account's UUID", + "type": "string" + }, + "channel": { + "default": "stable", + "description": "The channel.", + "type": [ + "null", + "string" + ] + }, + "platform": { + "description": "The target Platform. Is set programmatically explicitly during publishing.", + "type": [ + "null", + "string" + ] + }, + "product": { + "description": "Keygen product's UUID", + "type": "string" + }, + "provider": { + "description": "The provider. Must be `keygen`.", + "enum": [ + "keygen" + ], + "type": "string" + }, + "publishAutoUpdate": { + "default": true, + "description": "Whether to publish auto update info files.\n\nAuto update relies only on the first provider in the list (you can specify several publishers).\nThus, probably, there`s no need to upload the metadata files for the other configured providers. But by default will be uploaded.", + "type": "boolean" + }, + "publisherName": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "requestHeaders": { + "$ref": "#/definitions/OutgoingHttpHeaders", + "description": "Any custom request headers" + }, + "updaterCacheDirName": { + "type": [ + "null", + "string" + ] + } + }, + "required": [ + "account", + "product", + "provider" + ], + "type": "object" + }, "LinuxConfiguration": { "additionalProperties": false, "properties": { @@ -1744,6 +1882,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -1768,6 +1909,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -1990,6 +2134,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -2014,6 +2161,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -2478,6 +2628,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -2502,6 +2655,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -2622,11 +2778,11 @@ "description": "The target package type: list of `default`, `dmg`, `mas`, `mas-dev`, `pkg`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir`. Defaults to `default` (dmg and zip for Squirrel.Mac)." }, "timestamp": { + "description": "Specify the URL of the timestamp authority server", "type": [ "null", "string" - ], - "description": "Specify the URL of the timestamp authority server" + ] }, "type": { "anyOf": [ @@ -3077,6 +3233,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3101,6 +3260,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3221,11 +3383,11 @@ "description": "The target package type: list of `default`, `dmg`, `mas`, `mas-dev`, `pkg`, `7z`, `zip`, `tar.xz`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir`. Defaults to `default` (dmg and zip for Squirrel.Mac)." }, "timestamp": { + "description": "Specify the URL of the timestamp authority server", "type": [ "null", "string" - ], - "description": "Specify the URL of the timestamp authority server" + ] }, "type": { "anyOf": [ @@ -3278,6 +3440,20 @@ "MsiOptions": { "additionalProperties": false, "properties": { + "additionalWixArgs": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Any additional arguments to be passed to the WiX installer compiler, such as `[\"-ext\", \"WixUtilExtension\"]`" + }, "artifactName": { "description": "The [artifact file name template](/configuration/configuration#artifact-file-name-template).", "type": [ @@ -3337,6 +3513,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3361,6 +3540,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3402,13 +3584,6 @@ "default": true, "description": "If `warningsAsErrors` is `true` (default): treat warnings as errors. If `warningsAsErrors` is `false`: allow warnings.", "type": "boolean" - }, - "additionalWixArgs": { - "description": "Any additional arguments to be passed to the WiX installer compiler, such as `[\"-ext\", \"WixUtilExtension\"]`", - "type": [ - "null", - "array" - ] } }, "type": "object" @@ -3448,36 +3623,21 @@ "type": "boolean" }, "customNsisBinary": { - "description": "Allows providing the URL configuration for `makensis`.", - "type": [ - "null", - "object" - ], - "properties": { - "url": { - "description": "URL to download from", - "type": "string" - }, - "checksum": { - "description": "SHA256 to validate downloaded `makensis`", - "type": [ - "null", - "string" - ] + "anyOf": [ + { + "$ref": "#/definitions/CustomNsisBinary" }, - "version": { - "description": "Version of `makensis` (used for caching)", - "type": [ - "null", - "string" - ] + { + "type": "null" } - } + ] }, "debugLogging": { - "default": false, - "description": "Enables `LogText` to be used in `nsh` scripts", - "type": "boolean" + "description": "Whether or not to enable NSIS logging for debugging.\nNote: Requires a debug-enabled NSIS build.\nelectron-builder's included `makensis` only supports building debug-enabled NSIS installers on Windows currently\nhttps://github.com/electron-userland/electron-builder/issues/5119#issuecomment-811353612", + "type": [ + "null", + "boolean" + ] }, "deleteAppDataOnUninstall": { "default": false, @@ -3641,6 +3801,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3665,6 +3828,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3781,36 +3947,21 @@ "type": "boolean" }, "customNsisBinary": { - "description": "Allows providing the URL configuration for `makensis`.", - "type": [ - "null", - "object" - ], - "properties": { - "url": { - "description": "URL to download from", - "type": "string" - }, - "checksum": { - "description": "SHA256 to validate downloaded `makensis`", - "type": [ - "null", - "string" - ] + "anyOf": [ + { + "$ref": "#/definitions/CustomNsisBinary" }, - "version": { - "description": "Version of `makensis` (used for caching)", - "type": [ - "null", - "string" - ] + { + "type": "null" } - } + ] }, "debugLogging": { - "default": false, - "description": "Enables `LogText` to be used in `nsh` scripts", - "type": "boolean" + "description": "Whether or not to enable NSIS logging for debugging.\nNote: Requires a debug-enabled NSIS build.\nelectron-builder's included `makensis` only supports building debug-enabled NSIS installers on Windows currently\nhttps://github.com/electron-userland/electron-builder/issues/5119#issuecomment-811353612", + "type": [ + "null", + "boolean" + ] }, "deleteAppDataOnUninstall": { "default": false, @@ -3974,6 +4125,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -3998,6 +4152,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -4289,6 +4446,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -4313,6 +4473,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -4373,6 +4536,23 @@ "string" ] }, + "customNsisBinary": { + "anyOf": [ + { + "$ref": "#/definitions/CustomNsisBinary" + }, + { + "type": "null" + } + ] + }, + "debugLogging": { + "description": "Whether or not to enable NSIS logging for debugging.\nNote: Requires a debug-enabled NSIS build.\nelectron-builder's included `makensis` only supports building debug-enabled NSIS installers on Windows currently\nhttps://github.com/electron-userland/electron-builder/issues/5119#issuecomment-811353612", + "type": [ + "null", + "boolean" + ] + }, "guid": { "description": "See [GUID vs Application Name](../configuration/nsis#guid-vs-application-name).", "type": [ @@ -4400,6 +4580,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -4424,6 +4607,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -4465,7 +4651,7 @@ "type": "boolean" }, "unpackDirName": { - "description": "The unpack directory for the portable app resources.\nIf set to a string, it will be the name in [TEMP](https://www.askvg.com/where-does-windows-store-temporary-files-and-how-to-change-temp-folder-location/) directory.\nIf set explicitly to `false`, it will use the Windows temp directory ($PLUGINSDIR) that is unique to each launch of the portable application.\nDefaults to [uuid](https://github.com/segmentio/ksuid) of build (changed on each build of portable executable).", + "description": "The unpack directory for the portable app resources.\n\nIf set to a string, it will be the name in [TEMP](https://www.askvg.com/where-does-windows-store-temporary-files-and-how-to-change-temp-folder-location/) directory\nIf set explicitly to `false`, it will use the Windows temp directory ($PLUGINSDIR) that is unique to each launch of the portable application.\n\nDefaults to [uuid](https://github.com/segmentio/ksuid) of build (changed on each build of portable executable).", "type": [ "string", "boolean" @@ -4522,6 +4708,7 @@ "custom", "generic", "github", + "keygen", "s3", "snapStore", "spaces" @@ -4928,6 +5115,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -4952,6 +5142,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -5237,6 +5430,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -5261,6 +5457,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -5628,6 +5827,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -5652,6 +5854,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -6071,6 +6276,10 @@ ], "description": "macOS DMG options." }, + "electronBranding": { + "$ref": "#/definitions/ElectronBrandingOptions", + "description": "The branding used by Electron's distributables. This is needed if a fork has modified Electron's BRANDING.json file." + }, "electronCompile": { "description": "Whether to use [electron-compile](http://github.com/electron/electron-compile) to compile app. Defaults to `true` if `electron-compile` in the dependencies. And `false` if in the `devDependencies` or doesn't specified.", "type": "boolean" @@ -6086,10 +6295,6 @@ ], "description": "Returns the path to custom Electron build (e.g. `~/electron/out/R`). Zip files must follow the pattern `electron-v${version}-${platformName}-${arch}.zip`, otherwise it will be assumed to be an unpacked Electron app directory" }, - "electronBranding": { - "$ref": "#/definitions/ElectronBrandingOptions", - "description": "The branding used by Electron's distributables. This is needed if a fork has modified Electron's BRANDING.json file." - }, "electronDownload": { "$ref": "#/definitions/ElectronDownloadOptions", "description": "The [electron-download](https://github.com/electron-userland/electron-download#usage) options." @@ -6278,7 +6483,7 @@ }, "includeSubNodeModules": { "default": false, - "description": "Whether to include *all* of the submodules 'node_modules' directories.", + "description": "Whether to include *all* of the submodules node_modules directories", "type": "boolean" }, "launchUiVersion": { @@ -6507,6 +6712,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -6531,6 +6739,9 @@ { "$ref": "#/definitions/CustomPublishOptions" }, + { + "$ref": "#/definitions/KeygenOptions" + }, { "$ref": "#/definitions/SnapStoreOptions" }, @@ -6558,14 +6769,14 @@ "description": "Whether to build using Electron Build Service if target not supported on current OS.", "type": "boolean" }, - "removePackageScripts": { + "removePackageKeywords": { "default": true, - "description": "Whether to remove `scripts` field from `package.json` files.", + "description": "Whether to remove `keywords` field from `package.json` files.", "type": "boolean" }, - "removePackageKeywords": { + "removePackageScripts": { "default": true, - "description": "Whether to remove `keywords` field from `package.json` files.", + "description": "Whether to remove `scripts` field from `package.json` files.", "type": "boolean" }, "rpm": { diff --git a/packages/app-builder-lib/src/publish/BintrayPublisher.ts b/packages/app-builder-lib/src/publish/BintrayPublisher.ts index 88172d2a3a..2a97989ba1 100644 --- a/packages/app-builder-lib/src/publish/BintrayPublisher.ts +++ b/packages/app-builder-lib/src/publish/BintrayPublisher.ts @@ -1,5 +1,5 @@ import { Arch, InvalidConfigurationError, isEmptyOrSpaces, isTokenCharValid, log, toLinuxArchString } from "builder-util" -import { BintrayOptions, configureRequestOptions, HttpError } from "builder-util-runtime" +import { BintrayOptions, configureRequestOptions, HttpError, HttpExecutor } from "builder-util-runtime" import { BintrayClient, Version } from "builder-util-runtime/out/bintray" import { httpExecutor } from "builder-util/out/nodeHttpExecutor" import { ClientRequest, RequestOptions } from "http" @@ -79,17 +79,9 @@ export class BintrayPublisher extends HttpPublisher { options.headers!["X-Bintray-Debian-Component"] = this.client.component } - for (let attemptNumber = 0; ; attemptNumber++) { - try { - return await httpExecutor.doApiRequest(configureRequestOptions(options, this.client.auth), this.context.cancellationToken, requestProcessor) - } catch (e) { - if (attemptNumber < 3 && ((e instanceof HttpError && e.statusCode === 502) || e.code === "EPIPE")) { - continue - } - - throw e - } - } + return HttpExecutor.retryOnServerError(() => { + return httpExecutor.doApiRequest(configureRequestOptions(options, this.client.auth), this.context.cancellationToken, requestProcessor) + }) } //noinspection JSUnusedGlobalSymbols diff --git a/packages/app-builder-lib/src/publish/KeygenPublisher.ts b/packages/app-builder-lib/src/publish/KeygenPublisher.ts new file mode 100644 index 0000000000..9ce3b9e429 --- /dev/null +++ b/packages/app-builder-lib/src/publish/KeygenPublisher.ts @@ -0,0 +1,116 @@ +import { Arch, InvalidConfigurationError, log, isEmptyOrSpaces } from "builder-util" +import { httpExecutor } from "builder-util/out/nodeHttpExecutor" +import { ClientRequest, RequestOptions } from "http" +import { HttpPublisher, PublishContext } from "electron-publish" +import { KeygenOptions } from "builder-util-runtime/out/publishOptions" +import { configureRequestOptions, HttpExecutor, parseJson } from "builder-util-runtime" +import * as path from "path" +export class KeygenPublisher extends HttpPublisher { + readonly providerName = "keygen" + readonly hostname = "api.keygen.sh" + + private readonly info: KeygenOptions + private readonly auth: string + private readonly version: string + private readonly basePath: string + + constructor(context: PublishContext, info: KeygenOptions, version: string) { + super(context) + + const token = process.env.KEYGEN_TOKEN + if (isEmptyOrSpaces(token)) { + throw new InvalidConfigurationError(`Keygen token is not set using env "KEYGEN_TOKEN" (see https://www.electron.build/configuration/publish#KeygenOptions)`) + } + + this.info = info + this.auth = `Bearer ${token.trim()}` + this.version = version + this.basePath = `/v1/accounts/${this.info.account}/releases` + } + + protected doUpload( + fileName: string, + _arch: Arch, + dataLength: number, + requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _file?: string + ): Promise { + return HttpExecutor.retryOnServerError(async () => { + const { data, errors } = await this.upsertRelease(fileName, dataLength) + if (errors) { + throw new Error(`Keygen - Upserting release returned errors: ${JSON.stringify(errors)}`) + } + const releaseId = data?.id + if (!releaseId) { + log.warn({ file: fileName, reason: "UUID doesn't exist and was not created" }, "upserting release failed") + throw new Error(`Keygen - Upserting release returned no UUID: ${JSON.stringify(data)}`) + } + await this.uploadArtifact(releaseId, dataLength, requestProcessor) + return releaseId + }) + } + + private async uploadArtifact(releaseId: any, dataLength: number, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void) { + const upload: RequestOptions = { + hostname: this.hostname, + path: `${this.basePath}/${releaseId}/artifact`, + headers: { + Accept: "application/vnd.api+json", + "Content-Length": dataLength, + }, + } + await httpExecutor.doApiRequest(configureRequestOptions(upload, this.auth, "PUT"), this.context.cancellationToken, requestProcessor) + } + + private async upsertRelease(fileName: string, dataLength: number): Promise<{ data: any; errors: any }> { + const req: RequestOptions = { + hostname: this.hostname, + method: "PUT", + path: this.basePath, + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + }, + } + const data = { + data: { + type: "release", + attributes: { + filename: fileName, + filetype: path.extname(fileName), + filesize: dataLength, + version: this.version, + platform: this.info.platform, + channel: this.info.channel || "stable", + }, + relationships: { + product: { + data: { + type: "product", + id: this.info.product, + }, + }, + }, + }, + } + log.debug({ data: JSON.stringify(data) }, "Keygen upsert release") + return parseJson(httpExecutor.request(configureRequestOptions(req, this.auth, "PUT"), this.context.cancellationToken, data)) + } + + async deleteRelease(releaseId: string): Promise { + const req: RequestOptions = { + hostname: this.hostname, + path: `${this.basePath}/${releaseId}`, + headers: { + Accept: "application/vnd.api+json", + }, + } + await httpExecutor.request(configureRequestOptions(req, this.auth, "DELETE"), this.context.cancellationToken) + } + + toString() { + const { account, product, platform } = this.info + return `Keygen (account: ${account}, product: ${product}, platform: ${platform}, version: ${this.version})` + } +} diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index fac9df43cc..fb421341b6 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -7,6 +7,7 @@ import { getS3LikeProviderBaseUrl, GithubOptions, githubUrl, + KeygenOptions, PublishConfiguration, PublishProvider, } from "builder-util-runtime" @@ -29,6 +30,7 @@ import { expandMacro } from "../util/macroExpander" import { WinPackager } from "../winPackager" import { SnapStoreOptions, SnapStorePublisher } from "./SnapStorePublisher" import { createUpdateInfoTasks, UpdateInfoFileTask, writeUpdateInfoFiles } from "./updateInfoBuilder" +import { KeygenPublisher } from "./KeygenPublisher" const publishForPrWarning = "There are serious security concerns with PUBLISH_FOR_PULL_REQUEST=true (see the CircleCI documentation (https://circleci.com/docs/1.0/fork-pr-builds/) for details)" + @@ -297,6 +299,9 @@ export function createPublisher(context: PublishContext, version: string, publis case "bintray": return new BintrayPublisher(context, publishConfig as BintrayOptions, version, options) + case "keygen": + return new KeygenPublisher(context, publishConfig as KeygenOptions, version) + case "generic": return null @@ -321,6 +326,9 @@ function requireProviderClass(provider: string, packager: Packager): any | null case "generic": return null + case "keygen": + return KeygenPublisher + case "s3": return S3Publisher @@ -419,6 +427,8 @@ async function resolvePublishConfigurations( serviceName = "github" } else if (!isEmptyOrSpaces(process.env.BT_TOKEN)) { serviceName = "bintray" + } else if (!isEmptyOrSpaces(process.env.KEYGEN_TOKEN)) { + serviceName = "keygen" } if (serviceName != null) { @@ -499,6 +509,13 @@ async function getResolvedPublishConfig( return options } + if (provider === "keygen") { + return { + ...options, + platform: platformPackager?.platform.name, + } as KeygenOptions + } + const isGithub = provider === "github" if (!isGithub && provider !== "bintray") { return options diff --git a/packages/app-builder-lib/src/version.ts b/packages/app-builder-lib/src/version.ts index 27c2f2edc7..57ac90e39e 100644 --- a/packages/app-builder-lib/src/version.ts +++ b/packages/app-builder-lib/src/version.ts @@ -1 +1 @@ -export const PACKAGE_VERSION = "22.12.0" \ No newline at end of file +export const PACKAGE_VERSION = "22.12.0" diff --git a/packages/app-builder-lib/tsconfig-scheme.json b/packages/app-builder-lib/tsconfig-scheme.json new file mode 100644 index 0000000000..08d890db6c --- /dev/null +++ b/packages/app-builder-lib/tsconfig-scheme.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "outDir": "out", + "rootDir": "src", + }, + "jsdoc": { + "out": "../../scripts/jsdoc/out/builder-lib", + "examples": "../../scripts/jsdoc/examples", + "access": "public" + }, + "include": [ + "src/**/*.ts", + "../../typings/*.d.ts" + ], + "exclude": [ + "../../typings/electron.d.ts" + ] +} \ No newline at end of file diff --git a/packages/app-builder-lib/tsconfig.json b/packages/app-builder-lib/tsconfig.json index c530214962..f92361fe9e 100644 --- a/packages/app-builder-lib/tsconfig.json +++ b/packages/app-builder-lib/tsconfig.json @@ -1,14 +1,5 @@ { - "extends": "../tsconfig-base.json", - "compilerOptions": { - "outDir": "out", - "rootDir": "src" - }, - "jsdoc": { - "out": "../../scripts/jsdoc/out/builder-lib", - "examples": "../../scripts/jsdoc/examples", - "access": "public" - }, + "extends": "./tsconfig-scheme.json", "references": [ { "path": "../builder-util" @@ -19,12 +10,5 @@ { "path": "../electron-publish" } - ], - "include": [ - "src/**/*.ts", - "../../typings/*.d.ts" - ], - "exclude": [ - "../../typings/electron.d.ts" ] } \ No newline at end of file diff --git a/packages/builder-util-runtime/src/httpExecutor.ts b/packages/builder-util-runtime/src/httpExecutor.ts index fec3da8136..153aef3390 100644 --- a/packages/builder-util-runtime/src/httpExecutor.ts +++ b/packages/builder-util-runtime/src/httpExecutor.ts @@ -60,22 +60,40 @@ export class HttpError extends Error { this.name = "HttpError" ;(this as NodeJS.ErrnoException).code = `HTTP_ERROR_${statusCode}` } + + // Check if 500 error + isServerError() { + return this.statusCode % 500 <= 99 + } } export function parseJson(result: Promise) { return result.then(it => (it == null || it.length === 0 ? null : JSON.parse(it))) } -export abstract class HttpExecutor { +interface Request { + abort: () => void + end: () => void +} +export abstract class HttpExecutor { protected readonly maxRedirects = 10 request(options: RequestOptions, cancellationToken: CancellationToken = new CancellationToken(), data?: { [name: string]: any } | null): Promise { configureRequestOptions(options) - const encodedData = data == null ? undefined : Buffer.from(JSON.stringify(data)) + const json = data == null ? undefined : JSON.stringify(data) + const encodedData = json ? Buffer.from(json) : undefined if (encodedData != null) { - options.method = "post" - options.headers!["Content-Type"] = "application/json" - options.headers!["Content-Length"] = encodedData.length + debug(json!) + const { headers, ...opts } = options + options = { + method: "post", + headers: { + "Content-Type": "application/json", + "Content-Length": encodedData.length, + ...headers, + }, + ...opts, + } } return this.doApiRequest(options, cancellationToken, it => { ;(it as any).end(encodedData) @@ -85,7 +103,7 @@ export abstract class HttpExecutor { doApiRequest( options: RequestOptions, cancellationToken: CancellationToken, - requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void, + requestProcessor: (request: T, reject: (error: Error) => void) => void, redirectCount = 0 ): Promise { if (debug.enabled) { @@ -130,7 +148,7 @@ export abstract class HttpExecutor { resolve: (data?: any) => void, reject: (error: Error) => void, redirectCount: number, - requestProcessor: (request: REQUEST, reject: (error: Error) => void) => void + requestProcessor: (request: T, reject: (error: Error) => void) => void ) { if (debug.enabled) { debug(`Response: ${response.statusCode} ${response.statusMessage}, request options: ${safeStringifyJson(options)}`) @@ -144,7 +162,7 @@ export abstract class HttpExecutor { response, `method: ${options.method || "GET"} url: ${options.protocol || "https:"}//${options.hostname}${options.port ? `:${options.port}` : ""}${options.path} -Please double check that your authentication token is correct. Due to security reasons actual status maybe not reported, but 404. +Please double check that your authentication token is correct. Due to security reasons, actual status maybe not reported, but 404. ` ) ) @@ -176,7 +194,16 @@ Please double check that your authentication token is correct. Due to security r if (response.statusCode != null && response.statusCode >= 400) { const contentType = safeGetHeader(response, "content-type") const isJson = contentType != null && (Array.isArray(contentType) ? contentType.find(it => it.includes("json")) != null : contentType.includes("json")) - reject(createHttpError(response, isJson ? JSON.parse(data) : data)) + reject( + createHttpError( + response, + `method: ${options.method || "GET"} url: ${options.protocol || "https:"}//${options.hostname}${options.port ? `:${options.port}` : ""}${options.path} + + Data: + ${isJson ? JSON.stringify(JSON.parse(data)) : data} + ` + ) + ) } else { resolve(data.length === 0 ? null : data) } @@ -187,7 +214,7 @@ Please double check that your authentication token is correct. Due to security r } // noinspection JSUnusedLocalSymbols - abstract createRequest(options: any, callback: (response: any) => void): any + abstract createRequest(options: any, callback: (response: any) => void): T async downloadToBuffer(url: URL, options: DownloadOptions): Promise { return await options.cancellationToken.createPromise((resolve, reject, onCancel) => { @@ -255,7 +282,7 @@ Please double check that your authentication token is correct. Due to security r }) } - protected doDownload(requestOptions: any, options: DownloadCallOptions, redirectCount: number) { + protected doDownload(requestOptions: RequestOptions, options: DownloadCallOptions, redirectCount: number) { const request = this.createRequest(requestOptions, (response: IncomingMessage) => { if (response.statusCode! >= 400) { options.callback( @@ -310,7 +337,7 @@ Please double check that your authentication token is correct. Due to security r static prepareRedirectUrlOptions(redirectUrl: string, options: RequestOptions): RequestOptions { const newOptions = configureRequestOptionsFromUrl(redirectUrl, { ...options }) const headers = newOptions.headers - if (headers != null && headers.authorization != null && (headers.authorization as string).startsWith("token")) { + if (headers?.authorization) { const parsedNewUrl = new URL(redirectUrl) if (parsedNewUrl.hostname.endsWith(".amazonaws.com") || parsedNewUrl.searchParams.has("X-Amz-Credential")) { delete headers.authorization @@ -318,6 +345,19 @@ Please double check that your authentication token is correct. Due to security r } return newOptions } + + static retryOnServerError(task: () => Promise, maxRetries = 3) { + for (let attemptNumber = 0; ; attemptNumber++) { + try { + return task() + } catch (e) { + if (attemptNumber < maxRetries && ((e instanceof HttpError && e.isServerError()) || e.code === "EPIPE")) { + continue + } + throw e + } + } + } } export interface DownloadCallOptions { @@ -467,7 +507,7 @@ export function configureRequestOptions(options: RequestOptions, token?: string const headers = options.headers if (token != null) { - ;(headers as any).authorization = token.startsWith("Basic") ? token : `token ${token}` + ;(headers as any).authorization = token.startsWith("Basic") || token.startsWith("Bearer") ? token : `token ${token}` } if (headers["User-Agent"] == null) { headers["User-Agent"] = "electron-builder" @@ -489,6 +529,7 @@ export function safeStringifyJson(data: any, skippedNames?: Set) { data, (name, value) => { if ( + name.endsWith("Authorization") || name.endsWith("authorization") || name.endsWith("Password") || name.endsWith("PASSWORD") || diff --git a/packages/builder-util-runtime/src/index.ts b/packages/builder-util-runtime/src/index.ts index fba05904f2..17b8007e55 100644 --- a/packages/builder-util-runtime/src/index.ts +++ b/packages/builder-util-runtime/src/index.ts @@ -18,6 +18,7 @@ export { CustomPublishOptions, GenericServerOptions, GithubOptions, + KeygenOptions, PublishConfiguration, S3Options, SpacesOptions, diff --git a/packages/builder-util-runtime/src/publishOptions.ts b/packages/builder-util-runtime/src/publishOptions.ts index dce18a5c77..20debe366c 100644 --- a/packages/builder-util-runtime/src/publishOptions.ts +++ b/packages/builder-util-runtime/src/publishOptions.ts @@ -1,9 +1,9 @@ import { OutgoingHttpHeaders } from "http" -export type PublishProvider = "github" | "bintray" | "s3" | "spaces" | "generic" | "custom" | "snapStore" +export type PublishProvider = "github" | "bintray" | "s3" | "spaces" | "generic" | "custom" | "snapStore" | "keygen" // typescript-json-schema generates only PublishConfiguration if it is specified in the list, so, it is not added here -export type AllPublishOptions = string | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions | CustomPublishOptions +export type AllPublishOptions = string | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions | CustomPublishOptions | KeygenOptions export interface PublishConfiguration { /** @@ -146,6 +146,39 @@ export interface GenericServerOptions extends PublishConfiguration { readonly useMultipleRangeRequest?: boolean } +/** + * Keygen options. + * https://keygen.sh/ + * Define `KEYGEN_TOKEN` environment variable. + */ +export interface KeygenOptions extends PublishConfiguration { + /** + * The provider. Must be `keygen`. + */ + readonly provider: "keygen" + + /** + * Keygen account's UUID + */ + readonly account: string + + /** + * Keygen product's UUID + */ + readonly product: string + + /** + * The channel. + * @default stable + */ + readonly channel?: string | null + + /** + * The target Platform. Is set programmatically explicitly during publishing. + */ + readonly platform?: string | null +} + export interface BaseS3Options extends PublishConfiguration { /** * The update channel. diff --git a/packages/builder-util/src/nodeHttpExecutor.ts b/packages/builder-util/src/nodeHttpExecutor.ts index 9222d6e202..616e729d96 100644 --- a/packages/builder-util/src/nodeHttpExecutor.ts +++ b/packages/builder-util/src/nodeHttpExecutor.ts @@ -5,7 +5,7 @@ import * as https from "https" export class NodeHttpExecutor extends HttpExecutor { // noinspection JSMethodCanBeStatic // noinspection JSUnusedGlobalSymbols - createRequest(options: any, callback: (response: any) => void): any { + createRequest(options: any, callback: (response: any) => void): ClientRequest { return (options.protocol === "http:" ? httpRequest : https.request)(options, callback) } } diff --git a/packages/electron-publish/src/publisher.ts b/packages/electron-publish/src/publisher.ts index f4732f61da..4dc73a967b 100644 --- a/packages/electron-publish/src/publisher.ts +++ b/packages/electron-publish/src/publisher.ts @@ -84,7 +84,7 @@ export abstract class HttpPublisher extends Publisher { const fileStat = await stat(task.file) const progressBar = this.createProgressBar(fileName, fileStat.size) - await this.doUpload( + return this.doUpload( fileName, task.arch || Arch.x64, fileStat.size, diff --git a/packages/electron-updater/src/AppUpdater.ts b/packages/electron-updater/src/AppUpdater.ts index 9e0b6d5887..46c80ce347 100644 --- a/packages/electron-updater/src/AppUpdater.ts +++ b/packages/electron-updater/src/AppUpdater.ts @@ -91,6 +91,15 @@ export abstract class AppUpdater extends EventEmitter { */ requestHeaders: OutgoingHttpHeaders | null = null + /** + * Shortcut for explicitly adding auth tokens to request headers + */ + addAuthHeader(token: string) { + this.requestHeaders = Object.assign({}, this.requestHeaders, { + authorization: token, + }) + } + protected _logger: Logger = console // noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols diff --git a/packages/electron-updater/src/MacUpdater.ts b/packages/electron-updater/src/MacUpdater.ts index d87dae271c..82d6f1043f 100644 --- a/packages/electron-updater/src/MacUpdater.ts +++ b/packages/electron-updater/src/MacUpdater.ts @@ -51,7 +51,7 @@ export class MacUpdater extends AppUpdater { } // allow arm64 macs to install universal or rosetta2(x64) - https://github.com/electron-userland/electron-builder/pull/5524 - const isArm64 = (file: ResolvedUpdateFileInfo) => (file.url.pathname.includes("arm64") || file.info.url?.includes("arm64")) + const isArm64 = (file: ResolvedUpdateFileInfo) => file.url.pathname.includes("arm64") || file.info.url?.includes("arm64") if (files.some(isArm64)) { files = files.filter(file => (process.arch === "arm64" || isRosetta) === isArm64(file)) } diff --git a/packages/electron-updater/src/electronHttpExecutor.ts b/packages/electron-updater/src/electronHttpExecutor.ts index c8e963fd02..037d466723 100644 --- a/packages/electron-updater/src/electronHttpExecutor.ts +++ b/packages/electron-updater/src/electronHttpExecutor.ts @@ -47,7 +47,7 @@ export class ElectronHttpExecutor extends HttpExecutor { }) } - createRequest(options: any, callback: (response: any) => void): any { + createRequest(options: any, callback: (response: any) => void): Electron.ClientRequest { // fix (node 7+) for making electron updater work when using AWS private buckets, check if headers contain Host property if (options.headers && options.headers.Host) { // set host value from headers.Host @@ -64,7 +64,7 @@ export class ElectronHttpExecutor extends HttpExecutor { const request = require("electron").net.request({ ...options, session: this.cachedSession, - }) + }) as Electron.ClientRequest request.on("response", callback) if (this.proxyLoginCallback != null) { request.on("login", this.proxyLoginCallback) diff --git a/packages/electron-updater/src/providerFactory.ts b/packages/electron-updater/src/providerFactory.ts index 4bebe35405..fac0918b73 100644 --- a/packages/electron-updater/src/providerFactory.ts +++ b/packages/electron-updater/src/providerFactory.ts @@ -6,6 +6,7 @@ import { GenericServerOptions, getS3LikeProviderBaseUrl, GithubOptions, + KeygenOptions, newError, PublishConfiguration, } from "builder-util-runtime" @@ -13,6 +14,7 @@ import { AppUpdater } from "./AppUpdater" import { BintrayProvider } from "./providers/BintrayProvider" import { GenericProvider } from "./providers/GenericProvider" import { GitHubProvider } from "./providers/GitHubProvider" +import { KeygenProvider } from "./providers/KeygenProvider" import { PrivateGitHubProvider } from "./providers/PrivateGitHubProvider" import { Provider, ProviderRuntimeOptions } from "./providers/Provider" @@ -38,6 +40,9 @@ export function createClient(data: PublishConfiguration | AllPublishOptions, upd } } + case "keygen": + return new KeygenProvider(data as KeygenOptions, updater, runtimeOptions) + case "s3": case "spaces": return new GenericProvider( diff --git a/packages/electron-updater/src/providers/KeygenProvider.ts b/packages/electron-updater/src/providers/KeygenProvider.ts new file mode 100644 index 0000000000..970ffee085 --- /dev/null +++ b/packages/electron-updater/src/providers/KeygenProvider.ts @@ -0,0 +1,53 @@ +import { CancellationToken, KeygenOptions, newError, UpdateInfo } from "builder-util-runtime" +import { AppUpdater } from "../AppUpdater" +import { ResolvedUpdateFileInfo } from "../main" +import { getChannelFilename, newBaseUrl, newUrlFromBase } from "../util" +import { parseUpdateInfo, Provider, ProviderRuntimeOptions, resolveFiles } from "./Provider" + +export class KeygenProvider extends Provider { + private readonly baseUrl: URL + + constructor(private readonly configuration: KeygenOptions, private readonly updater: AppUpdater, runtimeOptions: ProviderRuntimeOptions) { + super({ + ...runtimeOptions, + isUseMultipleRangeRequest: false, + }) + this.baseUrl = newBaseUrl(`https://api.keygen.sh/v1/accounts/${this.configuration.account}/artifacts`) + } + + protected getDefaultChannelName() { + return this.getCustomChannelName("stable") + } + + private get channel(): string { + const result = this.updater.channel || this.configuration.channel + return result == null ? this.getDefaultChannelName() : this.getCustomChannelName(result) + } + + async getLatestVersion(): Promise { + const cancellationToken = new CancellationToken() + const channelFile = getChannelFilename(this.channel) + const channelUrl = newUrlFromBase(channelFile, this.baseUrl, this.updater.isAddNoCacheQuery) + try { + const updateInfo = await this.httpRequest( + channelUrl, + { + Accept: "application/vnd.api+json", + }, + cancellationToken + ) + return parseUpdateInfo(updateInfo, channelFile, channelUrl) + } catch (e) { + throw newError(`Unable to find latest version on ${this.toString()}, please ensure release exists: ${e.stack || e.message}`, "ERR_UPDATER_LATEST_VERSION_NOT_FOUND") + } + } + + resolveFiles(updateInfo: UpdateInfo): Array { + return resolveFiles(updateInfo, this.baseUrl) + } + + toString() { + const { account, product, platform } = this.configuration + return `Keygen (account: ${account}, product: ${product}, platform: ${platform}, channel: ${this.channel})` + } +} diff --git a/packages/electron-updater/src/providers/Provider.ts b/packages/electron-updater/src/providers/Provider.ts index f8f9ee0975..034830d04b 100644 --- a/packages/electron-updater/src/providers/Provider.ts +++ b/packages/electron-updater/src/providers/Provider.ts @@ -1,7 +1,8 @@ -import { CancellationToken, HttpExecutor, newError, safeStringifyJson, UpdateFileInfo, UpdateInfo, WindowsUpdateInfo, configureRequestUrl } from "builder-util-runtime" +import { CancellationToken, configureRequestUrl, newError, safeStringifyJson, UpdateFileInfo, UpdateInfo, WindowsUpdateInfo } from "builder-util-runtime" import { OutgoingHttpHeaders, RequestOptions } from "http" import { load } from "js-yaml" import { URL } from "url" +import { ElectronHttpExecutor } from "../electronHttpExecutor" import { ResolvedUpdateFileInfo } from "../main" import { newUrlFromBase } from "../util" @@ -11,12 +12,12 @@ export interface ProviderRuntimeOptions { isUseMultipleRangeRequest: boolean platform: ProviderPlatform - executor: HttpExecutor + executor: ElectronHttpExecutor } export abstract class Provider { private requestHeaders: OutgoingHttpHeaders | null = null - protected readonly executor: HttpExecutor + protected readonly executor: ElectronHttpExecutor protected constructor(private readonly runtimeOptions: ProviderRuntimeOptions) { this.executor = runtimeOptions.executor diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27b9030268..b4dc8e31b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: source-map-support: 0.5.19 ts-jsdoc: 3.2.2 typescript: 4.3.5 - typescript-json-schema: 0.50.1 + typescript-json-schema: ^0.50.1 v8-compile-cache: 2.3.0 dependencies: dmg-license: 1.0.9 @@ -572,19 +572,19 @@ packages: dependencies: '@babel/helper-get-function-arity': 7.14.5 '@babel/template': 7.14.5 - '@babel/types': 7.14.9 + '@babel/types': 7.15.0 /@babel/helper-get-function-arity/7.14.5: resolution: {integrity: sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.14.9 + '@babel/types': 7.15.0 /@babel/helper-hoist-variables/7.14.5: resolution: {integrity: sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.14.9 + '@babel/types': 7.15.0 /@babel/helper-member-expression-to-functions/7.14.7: resolution: {integrity: sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==} @@ -596,7 +596,7 @@ packages: resolution: {integrity: sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.14.9 + '@babel/types': 7.15.0 /@babel/helper-module-transforms/7.14.8: resolution: {integrity: sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA==} @@ -617,7 +617,7 @@ packages: resolution: {integrity: sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.14.9 + '@babel/types': 7.15.0 /@babel/helper-plugin-utils/7.14.5: resolution: {integrity: sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==} @@ -662,7 +662,7 @@ packages: resolution: {integrity: sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.14.9 + '@babel/types': 7.15.0 /@babel/helper-validator-identifier/7.14.9: resolution: {integrity: sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==} @@ -707,6 +707,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + /@babel/parser/7.15.3: + resolution: {integrity: sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==} + engines: {node: '>=6.0.0'} + hasBin: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.14.5_@babel+core@7.14.8: resolution: {integrity: sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ==} engines: {node: '>=6.9.0'} @@ -1750,8 +1755,8 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.14.5 - '@babel/parser': 7.14.9 - '@babel/types': 7.14.9 + '@babel/parser': 7.15.3 + '@babel/types': 7.15.0 /@babel/traverse/7.14.9: resolution: {integrity: sha512-bldh6dtB49L8q9bUyB7bC20UKgU+EFDwKJylwl234Kv+ySZeMD31Xeht6URyueQ6LrRRpF2tmkfcZooZR9/e8g==} @@ -1776,6 +1781,13 @@ packages: '@babel/helper-validator-identifier': 7.14.9 to-fast-properties: 2.0.0 + /@babel/types/7.15.0: + resolution: {integrity: sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.14.9 + to-fast-properties: 2.0.0 + /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2437,6 +2449,10 @@ packages: resolution: {integrity: sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==} dev: true + /@types/json-schema/7.0.9: + resolution: {integrity: sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==} + dev: true + /@types/lodash.escaperegexp/4.1.6: resolution: {integrity: sha512-uENiqxLlqh6RzeE1cC6Z2gHqakToN9vKlTVCFkSVjAfeMeh2fY0916tHwJHeeKs28qB/hGYvKuampGYH5QDVCw==} dependencies: @@ -2471,8 +2487,8 @@ packages: resolution: {integrity: sha512-YoTiIwdKxM3VLiY2sM05x4iGuTveYiCcDaUVmo1L5ndrXxPGW/NEoZu+pGcBirziomizcZsnsQoemikKcB2fRA==} dev: true - /@types/node/14.17.7: - resolution: {integrity: sha512-SYTdMaW47se8499q8m0fYKZZRlmq0RaRv6oYmlVm6DUm31l0fhOl1D03X8hGxohCKTI2Bg6w7W0TiYB51aJzag==} + /@types/node/14.17.11: + resolution: {integrity: sha512-n2OQ+0Bz6WEsUjrvcHD1xZ8K+Kgo4cn9/w94s1bJS690QMUWfJPW/m7CCb7gPkA1fcYwL2UpjXP/rq/Eo41m6w==} dev: true /@types/node/16.4.10: @@ -8709,8 +8725,8 @@ packages: resolution: {integrity: sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA==} hasBin: true dependencies: - '@types/json-schema': 7.0.8 - '@types/node': 14.17.7 + '@types/json-schema': 7.0.9 + '@types/node': 14.17.11 glob: 7.1.7 json-stable-stringify: 1.0.1 ts-node: 9.1.1_typescript@4.2.4 diff --git a/scripts/update-package-version-export.js b/scripts/update-package-version-export.js index 31bd09b22f..46320bc303 100755 --- a/scripts/update-package-version-export.js +++ b/scripts/update-package-version-export.js @@ -4,4 +4,6 @@ const version = require(path.join(__dirname, "../packages/app-builder-lib/packag const destFile = path.join(__dirname, '../packages/app-builder-lib/src/version.ts') const { writeFileSync } = require("fs") -writeFileSync(destFile, `export const PACKAGE_VERSION = "${version}"`) \ No newline at end of file +writeFileSync(destFile, ` +export const PACKAGE_VERSION = "${version}" +`) \ No newline at end of file diff --git a/test/jestSetup.js b/test/jestSetup.js index 888cdb8c0b..dbda1d445f 100644 --- a/test/jestSetup.js +++ b/test/jestSetup.js @@ -13,9 +13,12 @@ const skipSuite = describe.skip const isAllTests = process.env.ALL_TESTS !== "false" describe.ifAll = isAllTests ? describe : skipSuite test.ifAll = isAllTests ? test : skip - skip.ifAll = skip +const execEnv = (envVar) => !!envVar ? test : skip +test.ifEnv = execEnv +skip.ifEnv = execEnv + const isMac = process.platform === "darwin" test.ifMac = isMac ? test : skip diff --git a/test/snapshots/PublishManagerTest.js.snap b/test/snapshots/PublishManagerTest.js.snap index 4cceda8936..2196156e69 100644 --- a/test/snapshots/PublishManagerTest.js.snap +++ b/test/snapshots/PublishManagerTest.js.snap @@ -30,7 +30,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -47,7 +46,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -64,7 +62,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -81,7 +78,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -98,7 +94,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -115,7 +110,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -132,7 +126,6 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", "sha512": "@sha512", "size": "@size", "url": "TestApp-1.1.0-mac.zip", @@ -149,7 +142,14 @@ Object { "file": "Test App ßW-1.1.0-mac.zip", "safeArtifactName": "TestApp-1.1.0-mac.zip", "updateInfo": Object { - "blockMapSize": "@blockMapSize", + "sha512": "@sha512", + "size": "@size", + }, + }, + Object { + "file": "Test App ßW-1.1.0-mac.zip.blockmap", + "safeArtifactName": "Test App ßW-1.1.0-mac.zip.blockmap", + "updateInfo": Object { "sha512": "@sha512", "size": "@size", }, @@ -237,7 +237,22 @@ Object { "fileContent": Object { "files": Array [ Object { - "blockMapSize": "@blockMapSize", + "sha512": "@sha512", + "size": "@size", + "url": "Test App ßW_1.1.0_mac.zip", + }, + ], + "path": "Test App ßW_1.1.0_mac.zip", + "releaseDate": "@releaseDate", + "sha512": "@sha512", + "version": "1.1.0", + }, + }, + Object { + "file": "latest-mac.yml", + "fileContent": Object { + "files": Array [ + Object { "sha512": "@sha512", "size": "@size", "url": "Test App ßW_1.1.0_mac.zip", @@ -254,7 +269,14 @@ Object { "file": "Test App ßW_1.1.0_mac.zip", "safeArtifactName": "TestApp-1.1.0-mac.zip", "updateInfo": Object { - "blockMapSize": "@blockMapSize", + "sha512": "@sha512", + "size": "@size", + }, + }, + Object { + "file": "Test App ßW_1.1.0_mac.zip.blockmap", + "safeArtifactName": "Test App ßW_1.1.0_mac.zip.blockmap", + "updateInfo": Object { "sha512": "@sha512", "size": "@size", }, diff --git a/test/snapshots/configurationValidationTest.js.snap b/test/snapshots/configurationValidationTest.js.snap index e2268bbb89..c1433b9dd9 100644 --- a/test/snapshots/configurationValidationTest.js.snap +++ b/test/snapshots/configurationValidationTest.js.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`appId as object 1`] = ` +"Invalid configuration object. electron-builder has been initialized using a configuration object that does not match the API schema. + - configuration.appId should be: + null | string + -> The application id. Used as [CFBundleIdentifier](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070) for MacOS and as + [Application User Model ID](https://msdn.microsoft.com/en-us/library/windows/desktop/dd378459(v=vs.85).aspx) for Windows (NSIS target only, Squirrel.Windows not supported). It is strongly recommended that an explicit ID is set. + How to fix: + 1. Open https://www.electron.build/configuration/configuration + 2. Search the option name on the page (or type in into Search to find across the docs). + * Not found? The option was deprecated or not exists (check spelling). + * Found? Check that the option in the appropriate place. e.g. \\"title\\" only in the \\"dmg\\", not in the root. +" +`; + exports[`extraFiles 1`] = ` Object { "linux": Array [ diff --git a/test/snapshots/updater/nsisUpdaterTest.js.snap b/test/snapshots/updater/nsisUpdaterTest.js.snap index 97a7b10dc5..b92cbe5e3b 100644 --- a/test/snapshots/updater/nsisUpdaterTest.js.snap +++ b/test/snapshots/updater/nsisUpdaterTest.js.snap @@ -353,6 +353,30 @@ Array [ ] `; +exports[`file url keygen 1`] = ` +Object { + "files": Array [ + Object { + "sha512": "r0NdvVubzJWAN56nqjTdLkBWV/dq0HQOKz9A6QFplUozu3uSVXA2t731miy/S29mY4MD18iLxKD6GXnPfNNbNw==", + "size": 59865147, + "url": "electron-quick-start-typescript-1.0.0-x64.exe", + }, + ], + "path": "electron-quick-start-typescript-1.0.0-x64.exe", + "releaseDate": "2021-08-19T04:34:01.089Z", + "sha512": "r0NdvVubzJWAN56nqjTdLkBWV/dq0HQOKz9A6QFplUozu3uSVXA2t731miy/S29mY4MD18iLxKD6GXnPfNNbNw==", + "version": "1.0.0", +} +`; + +exports[`file url keygen 2`] = ` +Array [ + "checking-for-update", + "update-available", + "update-downloaded", +] +`; + exports[`invalid signature 1`] = `"ERR_UPDATER_INVALID_SIGNATURE"`; exports[`invalid signature 2`] = ` diff --git a/test/src/ArtifactPublisherTest.ts b/test/src/ArtifactPublisherTest.ts index c63c7b0fc4..f94f0e3771 100644 --- a/test/src/ArtifactPublisherTest.ts +++ b/test/src/ArtifactPublisherTest.ts @@ -1,10 +1,12 @@ import { Arch } from "builder-util" -import { CancellationToken, HttpError, S3Options, SpacesOptions } from "builder-util-runtime" -import { createPublisher } from "app-builder-lib/out/publish/PublishManager" +import { CancellationToken, HttpError, KeygenOptions, S3Options, SpacesOptions } from "builder-util-runtime" import { PublishContext } from "electron-publish" import { GitHubPublisher } from "electron-publish/out/gitHubPublisher" import { isCI as isCi } from "ci-info" import * as path from "path" +import { KeygenPublisher } from "app-builder-lib/out/publish/KeygenPublisher" +import { Platform } from "app-builder-lib" +import { createPublisher } from "app-builder-lib/out/publish/PublishManager" if (isCi && process.platform === "win32") { fit("Skip ArtifactPublisherTest suite on Windows CI", () => { @@ -80,28 +82,24 @@ testAndIgnoreApiRate("GitHub upload", async () => { } }) -if (process.env.AWS_ACCESS_KEY_ID != null && process.env.AWS_SECRET_ACCESS_KEY != null) { - test("S3 upload", async () => { - const publisher = createPublisher(publishContext, "0.0.1", { provider: "s3", bucket: "electron-builder-test" } as S3Options, {}, {} as any)!! - await publisher.upload({ file: iconPath, arch: Arch.x64 }) - // test overwrite - await publisher.upload({ file: iconPath, arch: Arch.x64 }) - }) -} +test.ifEnv(process.env.AWS_ACCESS_KEY_ID != null && process.env.AWS_SECRET_ACCESS_KEY != null)("S3 upload", async () => { + const publisher = createPublisher(publishContext, "0.0.1", { provider: "s3", bucket: "electron-builder-test" } as S3Options, {}, {} as any)! + await publisher.upload({ file: iconPath, arch: Arch.x64 }) + // test overwrite + await publisher.upload({ file: iconPath, arch: Arch.x64 }) +}) -if (process.env.DO_KEY_ID != null && process.env.DO_SECRET_KEY != null) { - test("DO upload", async () => { - const configuration: SpacesOptions = { - provider: "spaces", - name: "electron-builder-test", - region: "nyc3", - } - const publisher = createPublisher(publishContext, "0.0.1", configuration, {}, {} as any)!! - await publisher.upload({ file: iconPath, arch: Arch.x64 }) - // test overwrite - await publisher.upload({ file: iconPath, arch: Arch.x64 }) - }) -} +test.ifEnv(process.env.DO_KEY_ID != null && process.env.DO_SECRET_KEY != null)("DO upload", async () => { + const configuration: SpacesOptions = { + provider: "spaces", + name: "electron-builder-test", + region: "nyc3", + } + const publisher = createPublisher(publishContext, "0.0.1", configuration, {}, {} as any)! + await publisher.upload({ file: iconPath, arch: Arch.x64 }) + // test overwrite + await publisher.upload({ file: iconPath, arch: Arch.x64 }) +}) testAndIgnoreApiRate("prerelease", async () => { const publisher = new GitHubPublisher(publishContext, { provider: "github", owner: "actperepo", repo: "ecb2", token, releaseType: "prerelease" }, versionNumber()) @@ -126,3 +124,19 @@ testAndIgnoreApiRate("GitHub upload org", async () => { await publisher.deleteRelease() } }) + +test.ifEnv(process.env.KEYGEN_TOKEN)("Keygen upload", async () => { + const publisher = new KeygenPublisher( + publishContext, + { + provider: "keygen", + // electron-builder-test + product: "43981278-96e7-47de-b8c2-98d59987206b", + account: "cdecda36-3ef0-483e-ad88-97e7970f3149", + platform: Platform.MAC.name, + } as KeygenOptions, + versionNumber() + ) + const releaseId = await publisher.upload({ file: iconPath, arch: Arch.x64 }) + await publisher.deleteRelease(releaseId) +}) diff --git a/test/src/PublishManagerTest.ts b/test/src/PublishManagerTest.ts index 1c56a7df16..557631399f 100644 --- a/test/src/PublishManagerTest.ts +++ b/test/src/PublishManagerTest.ts @@ -1,25 +1,11 @@ import { createTargets, Platform } from "electron-builder" import { outputFile } from "fs-extra" import * as path from "path" -import { GithubOptions, GenericServerOptions, SpacesOptions } from "builder-util-runtime" +import { GithubOptions, GenericServerOptions, SpacesOptions, KeygenOptions } from "builder-util-runtime" import { assertThat } from "./helpers/fileAssert" import { app, checkDirContents } from "./helpers/packTester" -test.ifNotWindows.ifDevOrLinuxCi( - "generic, github and spaces", - app({ - targets: Platform.MAC.createTarget("zip"), - config: { - generateUpdatesFilesForAllChannels: true, - mac: { - electronUpdaterCompatibility: ">=2.16", - }, - publish: [genericPublisher("https://example.com/downloads"), githubPublisher("foo/foo"), spacesPublisher()], - }, - }) -) - -function spacesPublisher(publishAutoUpdate: boolean = true): SpacesOptions { +function spacesPublisher(publishAutoUpdate = true): SpacesOptions { return { provider: "spaces", name: "mySpaceName", @@ -42,6 +28,28 @@ function genericPublisher(url: string): GenericServerOptions { } } +function keygenPublisher(): KeygenOptions { + return { + provider: "keygen", + product: "43981278-96e7-47de-b8c2-98d59987206b", + account: "cdecda36-3ef0-483e-ad88-97e7970f3149", + } +} + +test.ifNotWindows.ifDevOrLinuxCi( + "generic, github and spaces", + app({ + targets: Platform.MAC.createTarget("zip"), + config: { + generateUpdatesFilesForAllChannels: true, + mac: { + electronUpdaterCompatibility: ">=2.16", + }, + publish: [genericPublisher("https://example.com/downloads"), githubPublisher("foo/foo"), spacesPublisher()], + }, + }) +) + test.ifNotWindows.ifDevOrLinuxCi( "github and spaces (publishAutoUpdate)", app({ @@ -66,7 +74,7 @@ test.ifMac( mac: { electronUpdaterCompatibility: ">=2.16", }, - publish: [spacesPublisher()], + publish: [spacesPublisher(), keygenPublisher()], }, }, { diff --git a/test/src/configurationValidationTest.ts b/test/src/configurationValidationTest.ts index 589df2549b..642fee9a80 100644 --- a/test/src/configurationValidationTest.ts +++ b/test/src/configurationValidationTest.ts @@ -21,7 +21,7 @@ test.ifAll.ifDevOrLinuxCi( ) ) -test.skip.ifDevOrLinuxCi( +test.ifDevOrLinuxCi( "appId as object", appThrows({ targets: linuxDirTarget, @@ -82,6 +82,6 @@ test.ifAll.ifDevOrLinuxCi("null string as null", async () => { const options = normalizeOptions(yargs.parse(["-c.mac.identity=null", "--config.mac.hardenedRuntime=false"]) as CliOptions) const config = options.config as Configuration await validateConfig(config, new DebugLogger()) - expect(config.mac!!.identity).toBeNull() - expect(config.mac!!.hardenedRuntime).toBe(false) + expect(config.mac!.identity).toBeNull() + expect(config.mac!.hardenedRuntime).toBe(false) }) diff --git a/test/src/helpers/updaterTestUtil.ts b/test/src/helpers/updaterTestUtil.ts index 98f879f963..6d38dcaf4c 100644 --- a/test/src/helpers/updaterTestUtil.ts +++ b/test/src/helpers/updaterTestUtil.ts @@ -1,8 +1,8 @@ -import { serializeToYaml, TmpDir, executeAppBuilder } from "builder-util" -import { BintrayOptions, GenericServerOptions, GithubOptions, S3Options, SpacesOptions, DownloadOptions } from "builder-util-runtime" +import { serializeToYaml, TmpDir } from "builder-util" +import { BintrayOptions, GenericServerOptions, GithubOptions, S3Options, SpacesOptions, DownloadOptions, KeygenOptions } from "builder-util-runtime" import { AppUpdater, NoOpLogger } from "electron-updater" import { MacUpdater } from "electron-updater/out/MacUpdater" -import { outputFile } from "fs-extra" +import { outputFile, writeFile } from "fs-extra" import * as path from "path" import { TestOnlyUpdaterOptions } from "electron-updater/out/AppUpdater" import { NsisUpdater } from "electron-updater/out/NsisUpdater" @@ -24,7 +24,7 @@ export async function createNsisUpdater(version: string = "0.0.1") { } // to reduce difference in test mode, setFeedURL is not used to set (NsisUpdater also read configOnDisk to load original publisherName) -export async function writeUpdateConfig(data: T): Promise { +export async function writeUpdateConfig(data: T): Promise { const updateConfigPath = path.join(await tmpDir.getTempDir({ prefix: "test-update-config" }), "app-update.yml") await outputFile(updateConfigPath, serializeToYaml(data)) return updateConfigPath @@ -61,12 +61,11 @@ export async function validateDownload(updater: AppUpdater, expectDownloadPromis } export class TestNodeHttpExecutor extends NodeHttpExecutor { - download(url: string, destination: string, options: DownloadOptions): Promise { - const args = ["download", "--url", url, "--output", destination] - if (options != null && options.sha512) { - args.push("--sha512", options.sha512) - } - return executeAppBuilder(args).then(() => destination) + async download(url: string, destination: string, options: DownloadOptions): Promise { + const obj = new URL(url) + const buffer = await this.downloadToBuffer(obj, options) + await writeFile(destination, buffer) + return buffer.toString() } } diff --git a/test/src/updater/nsisUpdaterTest.ts b/test/src/updater/nsisUpdaterTest.ts index 643389f5bf..eceb48664e 100644 --- a/test/src/updater/nsisUpdaterTest.ts +++ b/test/src/updater/nsisUpdaterTest.ts @@ -1,4 +1,4 @@ -import { GenericServerOptions, GithubOptions, S3Options, SpacesOptions } from "builder-util-runtime" +import { GenericServerOptions, GithubOptions, KeygenOptions, S3Options, SpacesOptions } from "builder-util-runtime" import { UpdateCheckResult } from "electron-updater" import { outputFile } from "fs-extra" import { tmpdir } from "os" @@ -46,6 +46,17 @@ test("file url generic", async () => { await validateDownload(updater) }) +test.ifEnv(process.env.KEYGEN_TOKEN)("file url keygen", async () => { + const updater = await createNsisUpdater() + updater.addAuthHeader(`Bearer ${process.env.KEYGEN_TOKEN}`) + updater.updateConfigPath = await writeUpdateConfig({ + provider: "keygen", + product: "43981278-96e7-47de-b8c2-98d59987206b", + account: "cdecda36-3ef0-483e-ad88-97e7970f3149", + }) + await validateDownload(updater) +}) + test.skip("DigitalOcean Spaces", async () => { const updater = await createNsisUpdater() updater.updateConfigPath = await writeUpdateConfig({ diff --git a/test/typings/jest-ex.d.ts b/test/typings/jest-ex.d.ts index 3fa4ad7884..1f72c4c0b8 100644 --- a/test/typings/jest-ex.d.ts +++ b/test/typings/jest-ex.d.ts @@ -17,6 +17,8 @@ declare module jest { ifLinuxOrDevMac: jest.It ifAll: jest.It + + ifEnv: (envVar: any) => jest.It } interface Describe { From 12320d8e48f8205c6c80ed25fefafc9a8c594654 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 22 Aug 2021 11:41:01 -0700 Subject: [PATCH 2/5] Adding .changeset for minor version bump --- .changeset/witty-kiwis-hope.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/witty-kiwis-hope.md diff --git a/.changeset/witty-kiwis-hope.md b/.changeset/witty-kiwis-hope.md new file mode 100644 index 0000000000..7a88907e9e --- /dev/null +++ b/.changeset/witty-kiwis-hope.md @@ -0,0 +1,9 @@ +--- +"app-builder-lib": minor +"builder-util": minor +"builder-util-runtime": minor +"electron-publish": minor +"electron-updater": minor +--- + +feat: Adding Keygen as an official publisher/updater for electron-builder (#6167) From 55e49b18bb3b70443ef4f5e440d5fba89fec47d5 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 22 Aug 2021 21:58:52 -0700 Subject: [PATCH 3/5] Only following location header on 3xx status codes --- packages/builder-util-runtime/src/httpExecutor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/builder-util-runtime/src/httpExecutor.ts b/packages/builder-util-runtime/src/httpExecutor.ts index 153aef3390..1cac32791b 100644 --- a/packages/builder-util-runtime/src/httpExecutor.ts +++ b/packages/builder-util-runtime/src/httpExecutor.ts @@ -173,8 +173,10 @@ Please double check that your authentication token is correct. Due to security r return } + const code = response.statusCode ?? 0 + const shouldRedirect = code >= 300 && code < 400 const redirectUrl = safeGetHeader(response, "location") - if (redirectUrl != null) { + if (shouldRedirect && redirectUrl != null) { if (redirectCount > this.maxRedirects) { reject(this.createMaxRedirectError()) return From fcb3d15ad352b2333b89ccc35269174d313a7337 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 23 Aug 2021 21:27:15 -0700 Subject: [PATCH 4/5] PR feedback --- packages/builder-util-runtime/src/httpExecutor.ts | 3 +-- packages/builder-util-runtime/src/publishOptions.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/builder-util-runtime/src/httpExecutor.ts b/packages/builder-util-runtime/src/httpExecutor.ts index 1cac32791b..5a89e018d5 100644 --- a/packages/builder-util-runtime/src/httpExecutor.ts +++ b/packages/builder-util-runtime/src/httpExecutor.ts @@ -61,9 +61,8 @@ export class HttpError extends Error { ;(this as NodeJS.ErrnoException).code = `HTTP_ERROR_${statusCode}` } - // Check if 500 error isServerError() { - return this.statusCode % 500 <= 99 + return this.statusCode >= 500 && this.statusCode <= 599 } } diff --git a/packages/builder-util-runtime/src/publishOptions.ts b/packages/builder-util-runtime/src/publishOptions.ts index 20debe366c..a7e7412ddd 100644 --- a/packages/builder-util-runtime/src/publishOptions.ts +++ b/packages/builder-util-runtime/src/publishOptions.ts @@ -171,7 +171,7 @@ export interface KeygenOptions extends PublishConfiguration { * The channel. * @default stable */ - readonly channel?: string | null + readonly channel?: "stable" | "rc" | "beta" | "alpha" | "dev" | null /** * The target Platform. Is set programmatically explicitly during publishing. From 0880d1bada5f626f0baf1a988e8f37e3647f398e Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 23 Aug 2021 22:25:56 -0700 Subject: [PATCH 5/5] fix: Channel name never has a suffix. Only the channel filename. --- .../electron-updater/src/providers/KeygenProvider.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/electron-updater/src/providers/KeygenProvider.ts b/packages/electron-updater/src/providers/KeygenProvider.ts index 970ffee085..c840a5742d 100644 --- a/packages/electron-updater/src/providers/KeygenProvider.ts +++ b/packages/electron-updater/src/providers/KeygenProvider.ts @@ -15,18 +15,13 @@ export class KeygenProvider extends Provider { this.baseUrl = newBaseUrl(`https://api.keygen.sh/v1/accounts/${this.configuration.account}/artifacts`) } - protected getDefaultChannelName() { - return this.getCustomChannelName("stable") - } - private get channel(): string { - const result = this.updater.channel || this.configuration.channel - return result == null ? this.getDefaultChannelName() : this.getCustomChannelName(result) + return this.updater.channel || this.configuration.channel || "stable" } async getLatestVersion(): Promise { const cancellationToken = new CancellationToken() - const channelFile = getChannelFilename(this.channel) + const channelFile = getChannelFilename(this.getCustomChannelName(this.channel)) const channelUrl = newUrlFromBase(channelFile, this.baseUrl, this.updater.isAddNoCacheQuery) try { const updateInfo = await this.httpRequest(