From 9e18cb14434bfd26fe4087ca0573ad8517be9a81 Mon Sep 17 00:00:00 2001 From: develar Date: Fri, 4 Nov 2016 06:55:31 +0100 Subject: [PATCH] feat: GitHub publish provider Closes #868 --- CONTRIBUTING.md | 9 +++- docs/Auto Update.md | 22 ++++++++++ docs/Docker.md | 4 +- nsis-auto-updater/src/GenericProvider.ts | 16 ++++--- nsis-auto-updater/src/GitHubProvider.ts | 56 ++++++++++++++++++++++++ nsis-auto-updater/src/NsisUpdater.ts | 14 +++--- src/publish/restApiRequest.ts | 23 +++++++++- test/src/nsisUpdaterTest.ts | 29 ++++++++++++ 8 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 docs/Auto Update.md create mode 100644 nsis-auto-updater/src/GitHubProvider.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36dc41d6469..dc79f4ece0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,4 +97,11 @@ Use one of the shared run configurations as a template and: ``` * Set `Environment Variables`: * `NODE_PATH` to `.`. - * Optionally, `TEST_APP_TMP_DIR` to some directory (e.g. `/tmp/electron-builder-test`) to inspect output if test uses temporary directory (only if `--match` is used). Specified directory will be used instead of random temporary directory and *cleared* on each run. \ No newline at end of file + * Optionally, `TEST_APP_TMP_DIR` to some directory (e.g. `/tmp/electron-builder-test`) to inspect output if test uses temporary directory (only if `--match` is used). Specified directory will be used instead of random temporary directory and *cleared* on each run. + +## Run Test using CLI +```sh +TEST_APP_TMP_DIR=/tmp/electron-builder-test NODE_PATH=. ./node_modules/.bin/ava --match="boring" test/out/nsisTest.js +``` + +where `TEST_APP_TMP_DIR` is specified to easily inspect and use test build, `boring` is the test name and `test/out/nsisTest.js` is the path to test file. \ No newline at end of file diff --git a/docs/Auto Update.md b/docs/Auto Update.md new file mode 100644 index 00000000000..58b5fd082dc --- /dev/null +++ b/docs/Auto Update.md @@ -0,0 +1,22 @@ +1. Install `electron-auto-updater` as app dependency. + +2. [Confugure publish](https://github.com/electron-userland/electron-builder/wiki/Options#buildpublish). + +3. Use `autoUpdater` from `electron-auto-updater` instead of `electron`, e.g. (ES 6): + + ```js + import {autoUpdater} from "electron-auto-updater" + ``` + + `electron-auto-updater` works in the same way as electron bundled, it allows you to avoid conditional statements and use the same API across platforms. + +4. Do not call `setFeedURL` on Windows. electron-builder automatically creates `app-update.yml` file for you on build in the `resources` (this file is internal, you don't need to be aware of it). But if need, you can — for example, to explicitly set `BintrayOptions`: + ```js + { + provider: "bintray", + owner: "actperepo", + package: "no-versions", + } + ``` + +Currently, `generic` (any HTTPS web server), `github` and `bintray` are supported. `latest.yml` will be generated in addition to installer for `generic` and `github` and must be uploaded also (in short: only `bintray` doesn't use `latest.yml` and this file must be not uploaded on Bintray). \ No newline at end of file diff --git a/docs/Docker.md b/docs/Docker.md index d9ef45ebcce..a42008e6656 100644 --- a/docs/Docker.md +++ b/docs/Docker.md @@ -12,4 +12,6 @@ Or to avoid second step, append to first command `/bin/bash -c "npm install && n If you don't need to build Windows, use image `electronuserland/electron-builder:latest` (wine is not installed in this image). -You can use `/test.sh` to install npm dependencies and run tests. \ No newline at end of file +You can use `/test.sh` to install npm dependencies and run tests. + +**NOTICE**: _Do not use Docker Toolbox on macOS._ Only [Docker for Mac](https://docs.docker.com/engine/installation/mac/#/docker-for-mac) works. \ No newline at end of file diff --git a/nsis-auto-updater/src/GenericProvider.ts b/nsis-auto-updater/src/GenericProvider.ts index 6d450dee2ca..9f0d65a5b56 100644 --- a/nsis-auto-updater/src/GenericProvider.ts +++ b/nsis-auto-updater/src/GenericProvider.ts @@ -24,12 +24,7 @@ export class GenericProvider implements Provider { throw e } - if (result.sha2 == null) { - throw new Error("Update info doesn't contain sha2 checksum") - } - if (result.path == null) { - throw new Error("Update info doesn't contain file path") - } + validateUpdateInfo(result) return result } @@ -40,4 +35,13 @@ export class GenericProvider implements Provider { sha2: versionInfo.sha2, } } +} + +export function validateUpdateInfo(info: UpdateInfo) { + if (info.sha2 == null) { + throw new Error("Update info doesn't contain sha2 checksum") + } + if (info.path == null) { + throw new Error("Update info doesn't contain file path") + } } \ No newline at end of file diff --git a/nsis-auto-updater/src/GitHubProvider.ts b/nsis-auto-updater/src/GitHubProvider.ts new file mode 100644 index 00000000000..d4e9a105e24 --- /dev/null +++ b/nsis-auto-updater/src/GitHubProvider.ts @@ -0,0 +1,56 @@ +import { Provider, FileInfo } from "./api" +import { VersionInfo, GithubOptions, UpdateInfo } from "../../src/options/publishOptions" +import { request, HttpError } from "../../src/publish/restApiRequest" +import { validateUpdateInfo } from "./GenericProvider" +import * as path from "path" + +export class GitHubProvider implements Provider { + constructor(private readonly options: GithubOptions) { + } + + async getLatestVersion(): Promise { + // do not use API to avoid limit + const basePath = this.getBasePath() + let version = (await request({hostname: "github.com", path: `${basePath}/latest`})).location + const versionPosition = version.lastIndexOf("/") + 1 + try { + version = version.substring(version[versionPosition] === "v" ? versionPosition + 1 : versionPosition) + } + catch (e) { + throw new Error(`Cannot parse extract version from location "${version}": ${e.stack || e.message}`) + } + + let result: UpdateInfo | null = null + try { + result = await request({hostname: "github.com", path: `https://github.com${basePath}/download/v${version}/latest.yml`}) + } + catch (e) { + if (e instanceof HttpError && e.response.statusCode === 404) { + throw new Error(`Cannot find latest.yml in the latest release artifacts: ${e.stack || e.message}`) + } + throw e + } + + validateUpdateInfo(result) + return result + } + + private getBasePath() { + return `/${this.options.owner}/${this.options.repo}/releases` + } + + async getUpdateFile(versionInfo: UpdateInfo): Promise { + const basePath = this.getBasePath() + // space is not supported on GitHub + const name = path.posix.basename(versionInfo.path).replace(/ /g, "-") + return { + name: name, + url: `https://github.com${basePath}/download/v${versionInfo.version}/${name}`, + sha2: versionInfo.sha2, + } + } +} + +interface Redirect { + readonly location: string +} \ No newline at end of file diff --git a/nsis-auto-updater/src/NsisUpdater.ts b/nsis-auto-updater/src/NsisUpdater.ts index 7132a3522e8..d54f249ad1d 100644 --- a/nsis-auto-updater/src/NsisUpdater.ts +++ b/nsis-auto-updater/src/NsisUpdater.ts @@ -11,6 +11,7 @@ import { BintrayOptions, PublishConfiguration, GithubOptions, GenericServerOptio import { readFile } from "fs-extra-p" import { safeLoad } from "js-yaml" import { GenericProvider } from "./GenericProvider" +import { GitHubProvider } from "./GitHubProvider" export class NsisUpdater extends EventEmitter { private setupPath: string | null @@ -154,14 +155,11 @@ function createClient(data: string | PublishConfiguration | BintrayOptions | Git } else { const provider = (data).provider - if (provider === "bintray") { - return new BintrayProvider(data) - } - else if (provider === "generic") { - return new GenericProvider(data) - } - else { - throw new Error(`Unsupported provider: ${provider}`) + switch (provider) { + case "github": return new GitHubProvider(data) + case "generic": return new GenericProvider(data) + case "bintray": return new BintrayProvider(data) + default: throw new Error(`Unsupported provider: ${provider}`) } } } diff --git a/src/publish/restApiRequest.ts b/src/publish/restApiRequest.ts index 23bbc8dd592..bac78a5812d 100644 --- a/src/publish/restApiRequest.ts +++ b/src/publish/restApiRequest.ts @@ -7,6 +7,7 @@ import { Url } from "url" import { safeLoad } from "js-yaml" import _debug from "debug" import Debugger = debug.Debugger +import { parse as parseUrl } from "url" const debug: Debugger = _debug("electron-builder") @@ -26,7 +27,7 @@ export function request(url: Url, token: string | null = null, data: { [name: } }, url) - if (url.hostname!!.includes("github")) { + if (url.hostname!!.includes("github") && !url.path!.endsWith(".yml")) { options.headers.Accept = "application/vnd.github.v3+json" } @@ -41,7 +42,7 @@ export function request(url: Url, token: string | null = null, data: { [name: return doApiRequest(options, token, it => it.end(encodedData)) } -export function doApiRequest(options: RequestOptions, token: string | null, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void): Promise { +export function doApiRequest(options: RequestOptions, token: string | null, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, redirectCount: number = 0): Promise { if (token != null) { (options.headers).authorization = token.startsWith("Basic") ? token : `token ${token}` } @@ -62,6 +63,24 @@ Please double check that your authentication token is correct. Due to security r return } + const redirectUrl = response.headers.location + if (redirectUrl != null) { + if (redirectCount > 10) { + reject(new Error("Too many redirects (> 10)")) + return + } + + if (options.path!.endsWith("/latest")) { + resolve({location: redirectUrl}) + } + else { + doApiRequest(Object.assign({}, options, parseUrl(redirectUrl)), token, requestProcessor) + .then(resolve) + .catch(reject) + } + return + } + let data = "" response.setEncoding("utf8") response.on("data", (chunk: string) => { diff --git a/test/src/nsisUpdaterTest.ts b/test/src/nsisUpdaterTest.ts index 52529d23234..41023ce8721 100644 --- a/test/src/nsisUpdaterTest.ts +++ b/test/src/nsisUpdaterTest.ts @@ -6,6 +6,7 @@ import { TmpDir } from "out/util/tmp" import { outputFile } from "fs-extra-p" import { safeDump } from "js-yaml" import { GenericServerOptions } from "out/options/publishOptions" +import { GithubOptions } from "out/options/publishOptions" const NsisUpdaterClass = require("../../nsis-auto-updater/out/nsis-auto-updater/src/NsisUpdater").NsisUpdater @@ -92,5 +93,33 @@ test("file url generic", async () => { }) assertThat(path.join(await updateCheckResult.downloadPromise)).isFile() + assertThat(actualEvents).isEqualTo(expectedEvents) +}) + +test("file url github", async () => { + const tmpDir = new TmpDir() + const testResourcesPath = await tmpDir.getTempFile("update-config") + await outputFile(path.join(testResourcesPath, "app-update.yml"), safeDump({ + provider: "github", + owner: "develar", + repo: "__test_nsis_release", + })) + g.__test_resourcesPath = testResourcesPath + const updater: NsisUpdater = new NsisUpdaterClass() + + const actualEvents: Array = [] + const expectedEvents = ["checking-for-update", "update-available", "update-downloaded"] + for (let eventName of expectedEvents) { + updater.addListener(eventName, () => { + actualEvents.push(eventName) + }) + } + + const updateCheckResult = await updater.checkForUpdates() + assertThat(updateCheckResult.fileInfo).hasProperties({ + url: "https://github.com/develar/__test_nsis_release/releases/download/v1.1.0/TestApp-Setup-1.1.0.exe" + }) + assertThat(path.join(await updateCheckResult.downloadPromise)).isFile() + assertThat(actualEvents).isEqualTo(expectedEvents) }) \ No newline at end of file