From a94532164709a545c0f6551fdc336dbc5377bda8 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Wed, 8 Sep 2021 10:03:06 -0700 Subject: [PATCH] feat: adding Bitbucket publisher and autoupdater (#6228) --- .changeset/shiny-colts-behave.md | 9 + docs/api/electron-builder.md | 4 +- docs/configuration/publish.md | 117 +++++++++--- docs/generated/DebOptions.md | 2 +- docs/generated/bitbucket-options.md | 16 ++ docs/generated/s3-options.md | 5 - docs/generated/snap-store-options.md | 17 -- docs/generated/spaces-options.md | 16 -- packages/app-builder-lib/package.json | 1 + packages/app-builder-lib/scheme.json | 171 ++++++++++++++++++ .../src/options/linuxOptions.ts | 3 +- .../src/publish/BitbucketPublisher.ts | 67 +++++++ .../src/publish/KeygenPublisher.ts | 2 +- .../src/publish/PublishManager.ts | 12 +- .../builder-util-runtime/src/httpExecutor.ts | 8 +- packages/builder-util-runtime/src/index.ts | 1 + .../src/publishOptions.ts | 69 ++++++- packages/electron-publish/src/publisher.ts | 4 +- .../electron-updater/src/providerFactory.ts | 5 + .../src/providers/BitbucketProvider.ts | 43 +++++ pnpm-lock.yaml | 11 ++ scripts/jsdoc2md2html.js | 3 +- test/src/ArtifactPublisherTest.ts | 13 +- test/src/helpers/updaterTestUtil.ts | 14 +- test/src/updater/nsisUpdaterTest.ts | 15 +- 25 files changed, 534 insertions(+), 94 deletions(-) create mode 100644 .changeset/shiny-colts-behave.md create mode 100644 docs/generated/bitbucket-options.md delete mode 100644 docs/generated/snap-store-options.md delete mode 100644 docs/generated/spaces-options.md create mode 100644 packages/app-builder-lib/src/publish/BitbucketPublisher.ts create mode 100644 packages/electron-updater/src/providers/BitbucketProvider.ts diff --git a/.changeset/shiny-colts-behave.md b/.changeset/shiny-colts-behave.md new file mode 100644 index 00000000000..ddb4be564be --- /dev/null +++ b/.changeset/shiny-colts-behave.md @@ -0,0 +1,9 @@ +--- +"app-builder-lib": minor +"builder-util-runtime": minor +"builder-util": minor +"electron-publish": minor +"electron-updater": minor +--- + +feat: adding Bitbucket publisher and autoupdater diff --git a/docs/api/electron-builder.md b/docs/api/electron-builder.md index 0965c871778..0128cf42c2a 100644 --- a/docs/api/electron-builder.md +++ b/docs/api/electron-builder.md @@ -1709,7 +1709,7 @@ return path.join(target.outDir, __${target.name}-${getArtifactArchName(arc options -PublishConfiguration | String | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions | module:builder-util-runtime/out/publishOptions.CustomPublishOptions | module:builder-util-runtime/out/publishOptions.KeygenOptions | SnapStoreOptions | String +PublishConfiguration | String | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions | module:builder-util-runtime/out/publishOptions.CustomPublishOptions | module:builder-util-runtime/out/publishOptions.KeygenOptions | SnapStoreOptions | module:builder-util-runtime/out/publishOptions.BitbucketOptions | String If you want to override configuration in the app-update.yml. @@ -1834,7 +1834,7 @@ This is different from the normal quit event sequence.

options -PublishConfiguration | String | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions | module:builder-util-runtime/out/publishOptions.CustomPublishOptions | module:builder-util-runtime/out/publishOptions.KeygenOptions | SnapStoreOptions | String +PublishConfiguration | String | GithubOptions | S3Options | SpacesOptions | GenericServerOptions | BintrayOptions | module:builder-util-runtime/out/publishOptions.CustomPublishOptions | module:builder-util-runtime/out/publishOptions.KeygenOptions | SnapStoreOptions | module:builder-util-runtime/out/publishOptions.BitbucketOptions | String If you want to override configuration in the app-update.yml. diff --git a/docs/configuration/publish.md b/docs/configuration/publish.md index 1f32c3d6869..a622f50e19d 100644 --- a/docs/configuration/publish.md +++ b/docs/configuration/publish.md @@ -12,23 +12,23 @@ If `KEYGEN_TOKEN` is defined and `GH_TOKEN` or `GITHUB_TOKEN` is not — default !!! info "Snap store" `snap` target by default publishes to snap store (the app store for Linux). To force publishing to another providers, explicitly specify publish configuration for `snap`. -You can publish to multiple providers. For example, to publish Windows artifacts to both GitHub and Bintray (order is important — first item will be used as a default auto-update server, so, in this example app will use github as auto-update provider): +You can publish to multiple providers. For example, to publish Windows artifacts to both GitHub and Bitbucket (order is important — first item will be used as a default auto-update server, so, in this example app will use github as auto-update provider): -```json tab="package.json" +```json { "build": { "win": { - "publish": ["github", "bintray"] + "publish": ["github", "bitbucket"] } } } ``` -```yaml tab="electron-builder.yaml" +```yaml win: publish: - github - - bintray + - bitbucket ``` You can also configure publishing using CLI arguments, for example, to force publishing snap not to Snap Store, but to GitHub: `-c.snap.publish=github` @@ -66,7 +66,7 @@ But please consider using automatic rules instead of explicitly specifying `publ Add to `scripts` in the development `package.json`: - ```json tab="package.json" + ```json "release": "electron-builder" ``` @@ -92,7 +92,7 @@ This example workflow is modelled on how releases are handled in maven (it is an 3. When you are ready to deploy, simply change you package version to `1.9.0` and push. This will then produce a `latest.yml` and `something.exe` on s3. Usually you'll git-tag this version as well (just to keep track of it). 4. Change the version back to a snapshot version right after, i.e. `1.10.0-snapshot`, and commit it. -## GitHub Repository and Bintray Package +## GitHub Repository Detected automatically using: @@ -103,6 +103,16 @@ Detected automatically using: * or `CIRCLE_PROJECT_USERNAME`/`CIRCLE_PROJECT_REPONAME`, * if no env, from `.git/config` origin url. +## Publishers +**Options Available:** +- GenericServerOptions +- GithubOptions +- SnapStoreOptions +- SpacesOptions +- KeygenOptions +- BitbucketOptions +- S3Options +

GenericServerOptions

Generic (any HTTP(S) server) options. @@ -216,27 +226,76 @@ Define KEYGEN_TOKEN environment variable.

requestHeaders module:http.OutgoingHttpHeaders - Any custom request headers

- - - -## S3Options -[Amazon S3](https://aws.amazon.com/s3/) options. - -AWS credentials are required, please see [getting your credentials](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html). -Define `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` [environment variables](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html). -Or in the [~/.aws/credentials](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html). - -Example configuration: - -```json tab="package.json" -{ - "build": - "publish": { - "provider": "s3", - "bucket": "bucket-name" - } - } +

BitbucketOptions

+

Bitbucket options. +https://bitbucket.org/ +Define BITBUCKET_TOKEN environment variable.

+

For converting an app password to a usable token, you can utilize this

+
convertAppPassword(owner: string, token: string) {
+const base64encodedData = Buffer.from(`${owner}:${token.trim()}`).toString("base64")
+return `Basic ${base64encodedData}`
 }
-```
+
+ +

Inherited from PublishConfiguration:

+ +

S3Options

+

Amazon S3 options. +AWS credentials are required, please see getting your credentials. +Define AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables. +Or in the ~/.aws/credentials.

+

Example configuration:

+
{
+"build":
+"publish": {
+"provider": "s3",
+"bucket": "bucket-name"
+}
+}
+}
+
+ -{!generated/s3-options.md!} + diff --git a/docs/generated/DebOptions.md b/docs/generated/DebOptions.md index d1d6d554db6..88f211290b7 100644 --- a/docs/generated/DebOptions.md +++ b/docs/generated/DebOptions.md @@ -1,5 +1,5 @@ diff --git a/docs/generated/bitbucket-options.md b/docs/generated/bitbucket-options.md new file mode 100644 index 00000000000..467979583e7 --- /dev/null +++ b/docs/generated/bitbucket-options.md @@ -0,0 +1,16 @@ + +

Inherited from PublishConfiguration:

+ diff --git a/docs/generated/s3-options.md b/docs/generated/s3-options.md index db794029f3c..6efd2230d0a 100644 --- a/docs/generated/s3-options.md +++ b/docs/generated/s3-options.md @@ -1,6 +1,3 @@ - - - - - diff --git a/docs/generated/snap-store-options.md b/docs/generated/snap-store-options.md deleted file mode 100644 index 059c1c051ee..00000000000 --- a/docs/generated/snap-store-options.md +++ /dev/null @@ -1,17 +0,0 @@ -

SnapStoreOptions

-

Snap Store options.

- -

Inherited from PublishConfiguration:

- diff --git a/docs/generated/spaces-options.md b/docs/generated/spaces-options.md deleted file mode 100644 index 43220861e10..00000000000 --- a/docs/generated/spaces-options.md +++ /dev/null @@ -1,16 +0,0 @@ - - - -

SpacesOptions

-

DigitalOcean Spaces options. -Access key is required, define DO_KEY_ID and DO_SECRET_KEY environment variables.

- - - diff --git a/packages/app-builder-lib/package.json b/packages/app-builder-lib/package.json index 6df0c86a735..3f527596aaa 100644 --- a/packages/app-builder-lib/package.json +++ b/packages/app-builder-lib/package.json @@ -60,6 +60,7 @@ "ejs": "^3.1.6", "electron-osx-sign": "^0.5.0", "electron-publish": "workspace:*", + "form-data": "^4.0.0", "fs-extra": "^10.0.0", "hosted-git-info": "^4.0.2", "is-ci": "^3.0.0", diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index ca9d4f3f612..305817cffcc 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -90,6 +90,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -117,6 +120,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -246,6 +252,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -273,6 +282,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -432,6 +444,69 @@ ], "type": "object" }, + "BitbucketOptions": { + "additionalProperties": false, + "description": "Bitbucket options.\nhttps://keygen.sh/\nDefine `BITBUCKET_TOKEN` environment variable.", + "properties": { + "channel": { + "default": "latest", + "description": "The channel.", + "type": [ + "null", + "string" + ] + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "provider": { + "description": "The provider. Must be `bitbucket`.", + "enum": [ + "bitbucket" + ], + "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" + }, + "slug": { + "description": "Repository slug/name", + "type": "string" + }, + "updaterCacheDirName": { + "type": [ + "null", + "string" + ] + } + }, + "required": [ + "owner", + "provider", + "slug" + ], + "type": "object" + }, "CustomNsisBinary": { "additionalProperties": false, "properties": { @@ -678,6 +753,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -705,6 +783,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -868,6 +949,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -895,6 +979,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -1247,6 +1334,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -1274,6 +1364,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -1899,6 +1992,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -1926,6 +2022,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -2151,6 +2250,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -2178,6 +2280,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -2645,6 +2750,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -2672,6 +2780,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -3250,6 +3361,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -3277,6 +3391,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -3530,6 +3647,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -3557,6 +3677,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -3818,6 +3941,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -3845,6 +3971,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -4142,6 +4271,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -4169,6 +4301,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -4463,6 +4598,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -4490,6 +4628,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -4597,6 +4738,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -4624,6 +4768,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -5119,6 +5266,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -5146,6 +5296,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -5442,6 +5595,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -5469,6 +5625,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -5839,6 +5998,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -5866,6 +6028,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } @@ -6738,6 +6903,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "items": { "anyOf": [ @@ -6765,6 +6933,9 @@ { "$ref": "#/definitions/SnapStoreOptions" }, + { + "$ref": "#/definitions/BitbucketOptions" + }, { "type": "string" } diff --git a/packages/app-builder-lib/src/options/linuxOptions.ts b/packages/app-builder-lib/src/options/linuxOptions.ts index 1f2e5b225a0..8b84e140c70 100644 --- a/packages/app-builder-lib/src/options/linuxOptions.ts +++ b/packages/app-builder-lib/src/options/linuxOptions.ts @@ -104,10 +104,11 @@ export interface LinuxTargetSpecificOptions extends CommonLinuxOptions, TargetSp */ readonly fpm?: Array | null } - export interface DebOptions extends LinuxTargetSpecificOptions { /** * Package dependencies. Defaults to `["gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3"]`. + * If need to support Debian, `libappindicator1` should be removed, it is [deprecated in Debian](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=895037). + * If need to support KDE, `gconf2` and `gconf-service` should be removed as it's no longer used by GNOME](https://packages.debian.org/bullseye/gconf2). */ readonly depends?: Array | null diff --git a/packages/app-builder-lib/src/publish/BitbucketPublisher.ts b/packages/app-builder-lib/src/publish/BitbucketPublisher.ts new file mode 100644 index 00000000000..78ba04c2b43 --- /dev/null +++ b/packages/app-builder-lib/src/publish/BitbucketPublisher.ts @@ -0,0 +1,67 @@ +import { Arch, InvalidConfigurationError, isEmptyOrSpaces } from "builder-util" +import { httpExecutor } from "builder-util/out/nodeHttpExecutor" +import { ClientRequest, RequestOptions } from "http" +import { HttpPublisher, PublishContext } from "electron-publish" +import { BitbucketOptions } from "builder-util-runtime/out/publishOptions" +import { configureRequestOptions, HttpExecutor } from "builder-util-runtime" +import * as FormData from "form-data" +import { readFile } from "fs/promises" +export class BitbucketPublisher extends HttpPublisher { + readonly providerName = "bitbucket" + readonly hostname = "api.bitbucket.org" + + private readonly info: BitbucketOptions + private readonly auth: string + private readonly basePath: string + + constructor(context: PublishContext, info: BitbucketOptions) { + super(context) + + const token = process.env.BITBUCKET_TOKEN + if (isEmptyOrSpaces(token)) { + throw new InvalidConfigurationError(`Bitbucket token is not set using env "BITBUCKET_TOKEN" (see https://www.electron.build/configuration/publish#BitbucketOptions)`) + } + this.info = info + this.auth = BitbucketPublisher.convertAppPassword(this.info.owner, token) + this.basePath = `/2.0/repositories/${this.info.owner}/${this.info.slug}/downloads` + } + + protected doUpload( + fileName: string, + _arch: Arch, + _dataLength: number, + _requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, + file: string + ): Promise { + return HttpExecutor.retryOnServerError(async () => { + const fileContent = await readFile(file) + const form = new FormData() + form.append("files", fileContent, fileName) + const upload: RequestOptions = { + hostname: this.hostname, + path: this.basePath, + headers: form.getHeaders(), + } + await httpExecutor.doApiRequest(configureRequestOptions(upload, this.auth, "POST"), this.context.cancellationToken, it => form.pipe(it)) + return fileName + }) + } + + async deleteRelease(filename: string): Promise { + const req: RequestOptions = { + hostname: this.hostname, + path: `${this.basePath}/${filename}`, + } + await httpExecutor.request(configureRequestOptions(req, this.auth, "DELETE"), this.context.cancellationToken) + } + + toString() { + const { owner, slug, channel } = this.info + return `Bitbucket (owner: ${owner}, slug: ${slug}, channel: ${channel})` + } + + static convertAppPassword(owner: string, token: string) { + const base64encodedData = Buffer.from(`${owner}:${token.trim()}`).toString("base64") + return `Basic ${base64encodedData}` + } +} diff --git a/packages/app-builder-lib/src/publish/KeygenPublisher.ts b/packages/app-builder-lib/src/publish/KeygenPublisher.ts index 9dde27710e1..3ad121a4f7a 100644 --- a/packages/app-builder-lib/src/publish/KeygenPublisher.ts +++ b/packages/app-builder-lib/src/publish/KeygenPublisher.ts @@ -35,7 +35,7 @@ export class KeygenPublisher extends HttpPublisher { dataLength: number, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _file?: string + _file: string ): Promise { return HttpExecutor.retryOnServerError(async () => { const { data, errors } = await this.upsertRelease(fileName, dataLength) diff --git a/packages/app-builder-lib/src/publish/PublishManager.ts b/packages/app-builder-lib/src/publish/PublishManager.ts index 6c299224336..7a95f4a99b5 100644 --- a/packages/app-builder-lib/src/publish/PublishManager.ts +++ b/packages/app-builder-lib/src/publish/PublishManager.ts @@ -32,6 +32,7 @@ import { WinPackager } from "../winPackager" import { SnapStorePublisher } from "./SnapStorePublisher" import { createUpdateInfoTasks, UpdateInfoFileTask, writeUpdateInfoFiles } from "./updateInfoBuilder" import { KeygenPublisher } from "./KeygenPublisher" +import { BitbucketPublisher } from "./BitbucketPublisher" 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)" + @@ -303,12 +304,12 @@ export function createPublisher(context: PublishContext, version: string, publis case "keygen": return new KeygenPublisher(context, publishConfig as KeygenOptions, version) - case "generic": - return null - case "snapStore": return new SnapStorePublisher(context, publishConfig as SnapStoreOptions) + case "generic": + return null + default: { const clazz = requireProviderClass(provider, packager) return clazz == null ? null : new clazz(context, publishConfig) @@ -339,6 +340,9 @@ function requireProviderClass(provider: string, packager: Packager): any | null case "spaces": return SpacesPublisher + case "bitbucket": + return BitbucketPublisher + default: { const name = `electron-publisher-${provider}` let module: any = null @@ -430,6 +434,8 @@ async function resolvePublishConfigurations( serviceName = "bintray" } else if (!isEmptyOrSpaces(process.env.KEYGEN_TOKEN)) { serviceName = "keygen" + } else if (!isEmptyOrSpaces(process.env.BITBUCKET_TOKEN)) { + serviceName = "bitbucket" } if (serviceName != null) { diff --git a/packages/builder-util-runtime/src/httpExecutor.ts b/packages/builder-util-runtime/src/httpExecutor.ts index 5a89e018d5c..7a304649e7e 100644 --- a/packages/builder-util-runtime/src/httpExecutor.ts +++ b/packages/builder-util-runtime/src/httpExecutor.ts @@ -72,7 +72,7 @@ export function parseJson(result: Promise) { interface Request { abort: () => void - end: () => void + end: (data?: Buffer) => void } export abstract class HttpExecutor { protected readonly maxRedirects = 10 @@ -94,9 +94,7 @@ export abstract class HttpExecutor { ...opts, } } - return this.doApiRequest(options, cancellationToken, it => { - ;(it as any).end(encodedData) - }) + return this.doApiRequest(options, cancellationToken, it => it.end(encodedData)) } doApiRequest( @@ -499,7 +497,7 @@ function configurePipes(options: DownloadCallOptions, response: IncomingMessage) }) } -export function configureRequestOptions(options: RequestOptions, token?: string | null, method?: "GET" | "DELETE" | "PUT"): RequestOptions { +export function configureRequestOptions(options: RequestOptions, token?: string | null, method?: "GET" | "DELETE" | "PUT" | "POST"): RequestOptions { if (method != null) { options.method = method } diff --git a/packages/builder-util-runtime/src/index.ts b/packages/builder-util-runtime/src/index.ts index 4b148906508..4893a9fb1d3 100644 --- a/packages/builder-util-runtime/src/index.ts +++ b/packages/builder-util-runtime/src/index.ts @@ -19,6 +19,7 @@ export { GenericServerOptions, GithubOptions, KeygenOptions, + BitbucketOptions, SnapStoreOptions, PublishConfiguration, S3Options, diff --git a/packages/builder-util-runtime/src/publishOptions.ts b/packages/builder-util-runtime/src/publishOptions.ts index bdacaa9f821..847865ad589 100644 --- a/packages/builder-util-runtime/src/publishOptions.ts +++ b/packages/builder-util-runtime/src/publishOptions.ts @@ -1,9 +1,19 @@ import { OutgoingHttpHeaders } from "http" -export type PublishProvider = "github" | "bintray" | "s3" | "spaces" | "generic" | "custom" | "snapStore" | "keygen" +export type PublishProvider = "github" | "bintray" | "s3" | "spaces" | "generic" | "custom" | "snapStore" | "keygen" | "bitbucket" // 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 | KeygenOptions | SnapStoreOptions +export type AllPublishOptions = + | string + | GithubOptions + | S3Options + | SpacesOptions + | GenericServerOptions + | BintrayOptions + | CustomPublishOptions + | KeygenOptions + | SnapStoreOptions + | BitbucketOptions export interface PublishConfiguration { /** @@ -179,6 +189,42 @@ export interface KeygenOptions extends PublishConfiguration { readonly platform?: string | null } +/** + * Bitbucket options. + * https://bitbucket.org/ + * Define `BITBUCKET_TOKEN` environment variable. + * + * For converting an app password to a usable token, you can utilize this +```typescript +convertAppPassword(owner: string, token: string) { + const base64encodedData = Buffer.from(`${owner}:${token.trim()}`).toString("base64") + return `Basic ${base64encodedData}` +} +``` + */ +export interface BitbucketOptions extends PublishConfiguration { + /** + * The provider. Must be `bitbucket`. + */ + readonly provider: "bitbucket" + + /** + * Repository owner + */ + readonly owner: string + + /** + * Repository slug/name + */ + readonly slug: string + + /** + * The channel. + * @default latest + */ + readonly channel?: string | null +} + /** * [Snap Store](https://snapcraft.io/) options. */ @@ -221,6 +267,25 @@ export interface BaseS3Options extends PublishConfiguration { readonly acl?: "private" | "public-read" | null } +/** + * [Amazon S3](https://aws.amazon.com/s3/) options. + * AWS credentials are required, please see [getting your credentials](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html). + * Define `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` [environment variables](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html). + * Or in the [~/.aws/credentials](http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html). + * + * Example configuration: + * +```json +{ + "build": + "publish": { + "provider": "s3", + "bucket": "bucket-name" + } + } +} +``` + */ export interface S3Options extends BaseS3Options { /** * The provider. Must be `s3`. diff --git a/packages/electron-publish/src/publisher.ts b/packages/electron-publish/src/publisher.ts index 4dc73a967bf..3b2537c95c5 100644 --- a/packages/electron-publish/src/publisher.ts +++ b/packages/electron-publish/src/publisher.ts @@ -77,7 +77,7 @@ export abstract class HttpPublisher extends Publisher { const fileName = (this.useSafeArtifactName ? task.safeArtifactName : null) || basename(task.file) if (task.fileContent != null) { - await this.doUpload(fileName, task.arch || Arch.x64, task.fileContent.length, it => it.end(task.fileContent)) + await this.doUpload(fileName, task.arch || Arch.x64, task.fileContent.length, it => it.end(task.fileContent), task.file) return } @@ -104,7 +104,7 @@ export abstract class HttpPublisher extends Publisher { arch: Arch, dataLength: number, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, - file?: string + file: string ): Promise } diff --git a/packages/electron-updater/src/providerFactory.ts b/packages/electron-updater/src/providerFactory.ts index 87e3d4c8e01..a9f7e93963c 100644 --- a/packages/electron-updater/src/providerFactory.ts +++ b/packages/electron-updater/src/providerFactory.ts @@ -2,6 +2,7 @@ import { AllPublishOptions, BaseS3Options, BintrayOptions, + BitbucketOptions, CustomPublishOptions, GenericServerOptions, getS3LikeProviderBaseUrl, @@ -12,6 +13,7 @@ import { } from "builder-util-runtime" import { AppUpdater } from "./AppUpdater" import { BintrayProvider } from "./providers/BintrayProvider" +import { BitbucketProvider } from "./providers/BitbucketProvider" import { GenericProvider } from "./providers/GenericProvider" import { GitHubProvider } from "./providers/GitHubProvider" import { KeygenProvider } from "./providers/KeygenProvider" @@ -40,6 +42,9 @@ export function createClient(data: PublishConfiguration | AllPublishOptions, upd } } + case "bitbucket": + return new BitbucketProvider(data as BitbucketOptions, updater, runtimeOptions) + case "keygen": return new KeygenProvider(data as KeygenOptions, updater, runtimeOptions) diff --git a/packages/electron-updater/src/providers/BitbucketProvider.ts b/packages/electron-updater/src/providers/BitbucketProvider.ts new file mode 100644 index 00000000000..82521282601 --- /dev/null +++ b/packages/electron-updater/src/providers/BitbucketProvider.ts @@ -0,0 +1,43 @@ +import { CancellationToken, BitbucketOptions, 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 BitbucketProvider extends Provider { + private readonly baseUrl: URL + + constructor(private readonly configuration: BitbucketOptions, private readonly updater: AppUpdater, runtimeOptions: ProviderRuntimeOptions) { + super({ + ...runtimeOptions, + isUseMultipleRangeRequest: false, + }) + const { owner, slug } = configuration + this.baseUrl = newBaseUrl(`https://api.bitbucket.org/2.0/repositories/${owner}/${slug}/downloads`) + } + + private get channel(): string { + return this.updater.channel || this.configuration.channel || "latest" + } + + async getLatestVersion(): Promise { + const cancellationToken = new CancellationToken() + const channelFile = getChannelFilename(this.getCustomChannelName(this.channel)) + const channelUrl = newUrlFromBase(channelFile, this.baseUrl, this.updater.isAddNoCacheQuery) + try { + const updateInfo = await this.httpRequest(channelUrl, undefined, 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 { owner, slug } = this.configuration + return `Bitbucket (owner: ${owner}, slug: ${slug}, channel: ${this.channel})` + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ee758cffbc..2bfefeaba63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,7 @@ importers: electron-builder-squirrel-windows: workspace:* electron-osx-sign: ^0.5.0 electron-publish: workspace:* + form-data: ^4.0.0 fs-extra: ^10.0.0 hosted-git-info: ^4.0.2 is-ci: ^3.0.0 @@ -128,6 +129,7 @@ importers: ejs: 3.1.6 electron-osx-sign: 0.5.0 electron-publish: link:../electron-publish + form-data: 4.0.0 fs-extra: 10.0.0 hosted-git-info: 4.0.2 is-ci: 3.0.0 @@ -5610,6 +5612,15 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.32 + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.32 + dev: false + /fragment-cache/0.2.1: resolution: {integrity: sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=} engines: {node: '>=0.10.0'} diff --git a/scripts/jsdoc2md2html.js b/scripts/jsdoc2md2html.js index d49679545bd..32fbcaf2168 100644 --- a/scripts/jsdoc2md2html.js +++ b/scripts/jsdoc2md2html.js @@ -215,8 +215,9 @@ async function render2(files, jsdoc2MdOptions) { "SnapStoreOptions": "", "SpacesOptions": "", "KeygenOptions": "", + "BitbucketOptions": "", + "S3Options": "" }), - new Page("generated/s3-options.md", "S3Options"), new Page("generated/appimage-options.md", "AppImageOptions"), new Page("generated/DebOptions.md", "DebOptions"), diff --git a/test/src/ArtifactPublisherTest.ts b/test/src/ArtifactPublisherTest.ts index f94f0e37713..a99512708f5 100644 --- a/test/src/ArtifactPublisherTest.ts +++ b/test/src/ArtifactPublisherTest.ts @@ -1,5 +1,5 @@ import { Arch } from "builder-util" -import { CancellationToken, HttpError, KeygenOptions, S3Options, SpacesOptions } from "builder-util-runtime" +import { BitbucketOptions, 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" @@ -7,6 +7,7 @@ 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" +import { BitbucketPublisher } from "app-builder-lib/out/publish/BitbucketPublisher" if (isCi && process.platform === "win32") { fit("Skip ArtifactPublisherTest suite on Windows CI", () => { @@ -140,3 +141,13 @@ test.ifEnv(process.env.KEYGEN_TOKEN)("Keygen upload", async () => { const releaseId = await publisher.upload({ file: iconPath, arch: Arch.x64 }) await publisher.deleteRelease(releaseId) }) + +test.ifEnv(process.env.BITBUCKET_TOKEN)("Bitbucket upload", async () => { + const publisher = new BitbucketPublisher(publishContext, { + provider: "bitbucket", + owner: "mike-m", + slug: "electron-builder-test", + } as BitbucketOptions) + const filename = await publisher.upload({ file: iconPath, arch: Arch.x64 }) + await publisher.deleteRelease(filename) +}) diff --git a/test/src/helpers/updaterTestUtil.ts b/test/src/helpers/updaterTestUtil.ts index 6d38dcaf4c2..b0a002ec029 100644 --- a/test/src/helpers/updaterTestUtil.ts +++ b/test/src/helpers/updaterTestUtil.ts @@ -1,5 +1,5 @@ import { serializeToYaml, TmpDir } from "builder-util" -import { BintrayOptions, GenericServerOptions, GithubOptions, S3Options, SpacesOptions, DownloadOptions, KeygenOptions } from "builder-util-runtime" +import { DownloadOptions, AllPublishOptions } from "builder-util-runtime" import { AppUpdater, NoOpLogger } from "electron-updater" import { MacUpdater } from "electron-updater/out/MacUpdater" import { outputFile, writeFile } from "fs-extra" @@ -12,19 +12,19 @@ import { NodeHttpExecutor } from "builder-util/out/nodeHttpExecutor" const tmpDir = new TmpDir("updater-test-util") -export async function createTestAppAdapter(version: string = "0.0.1") { +export async function createTestAppAdapter(version = "0.0.1") { return new TestAppAdapter(version, await tmpDir.getTempDir()) } -export async function createNsisUpdater(version: string = "0.0.1") { +export async function createNsisUpdater(version = "0.0.1") { const testAppAdapter = await createTestAppAdapter(version) const result = new NsisUpdater(null, testAppAdapter) - await tuneTestUpdater(result) + tuneTestUpdater(result) return result } // 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 @@ -49,7 +49,7 @@ export async function validateDownload(updater: AppUpdater, expectDownloadPromis if (updater instanceof MacUpdater) { expect(downloadResult).toEqual([]) } else { - await assertThat(path.join(downloadResult!![0])).isFile() + await assertThat(path.join(downloadResult![0])).isFile() } } else { // noinspection JSIgnoredPromiseFromCall @@ -71,7 +71,7 @@ export class TestNodeHttpExecutor extends NodeHttpExecutor { export const httpExecutor: TestNodeHttpExecutor = new TestNodeHttpExecutor() -export async function tuneTestUpdater(updater: AppUpdater, options?: TestOnlyUpdaterOptions) { +export function tuneTestUpdater(updater: AppUpdater, options?: TestOnlyUpdaterOptions) { ;(updater as any).httpExecutor = httpExecutor ;(updater as any)._testOnlyOptions = { platform: "win32", diff --git a/test/src/updater/nsisUpdaterTest.ts b/test/src/updater/nsisUpdaterTest.ts index eceb48664e0..5dcb09eb814 100644 --- a/test/src/updater/nsisUpdaterTest.ts +++ b/test/src/updater/nsisUpdaterTest.ts @@ -1,4 +1,5 @@ -import { GenericServerOptions, GithubOptions, KeygenOptions, S3Options, SpacesOptions } from "builder-util-runtime" +import { BitbucketPublisher } from "app-builder-lib/out/publish/BitbucketPublisher" +import { BitbucketOptions, GenericServerOptions, GithubOptions, KeygenOptions, S3Options, SpacesOptions } from "builder-util-runtime" import { UpdateCheckResult } from "electron-updater" import { outputFile } from "fs-extra" import { tmpdir } from "os" @@ -57,6 +58,18 @@ test.ifEnv(process.env.KEYGEN_TOKEN)("file url keygen", async () => { await validateDownload(updater) }) +test.ifEnv(process.env.BITBUCKET_TOKEN)("file url bitbucket", async () => { + const updater = await createNsisUpdater() + const options: BitbucketOptions = { + provider: "bitbucket", + owner: "mike-m", + slug: "electron-builder-test", + } + updater.addAuthHeader(BitbucketPublisher.convertAppPassword(options.owner, process.env.BITBUCKET_TOKEN!)) + updater.updateConfigPath = await writeUpdateConfig(options) + await validateDownload(updater) +}) + test.skip("DigitalOcean Spaces", async () => { const updater = await createNsisUpdater() updater.updateConfigPath = await writeUpdateConfig({