Skip to content

Commit

Permalink
feat: GitHub publish provider
Browse files Browse the repository at this point in the history
Closes #868
  • Loading branch information
develar committed Nov 4, 2016
1 parent c5627f8 commit 9e18cb1
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 18 deletions.
9 changes: 8 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* 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.
22 changes: 22 additions & 0 deletions docs/Auto Update.md
Original file line number Diff line number Diff line change
@@ -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).
4 changes: 3 additions & 1 deletion docs/Docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
16 changes: 10 additions & 6 deletions nsis-auto-updater/src/GenericProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ export class GenericProvider implements Provider<UpdateInfo> {
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
}

Expand All @@ -40,4 +35,13 @@ export class GenericProvider implements Provider<UpdateInfo> {
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")
}
}
56 changes: 56 additions & 0 deletions nsis-auto-updater/src/GitHubProvider.ts
Original file line number Diff line number Diff line change
@@ -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<VersionInfo> {
constructor(private readonly options: GithubOptions) {
}

async getLatestVersion(): Promise<UpdateInfo> {
// do not use API to avoid limit
const basePath = this.getBasePath()
let version = (await request<Redirect>({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<UpdateInfo>({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<FileInfo> {
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
}
14 changes: 6 additions & 8 deletions nsis-auto-updater/src/NsisUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -154,14 +155,11 @@ function createClient(data: string | PublishConfiguration | BintrayOptions | Git
}
else {
const provider = (<PublishConfiguration>data).provider
if (provider === "bintray") {
return new BintrayProvider(<BintrayOptions>data)
}
else if (provider === "generic") {
return new GenericProvider(<GenericServerOptions>data)
}
else {
throw new Error(`Unsupported provider: ${provider}`)
switch (provider) {
case "github": return new GitHubProvider(<GithubOptions>data)
case "generic": return new GenericProvider(<GenericServerOptions>data)
case "bintray": return new BintrayProvider(<BintrayOptions>data)
default: throw new Error(`Unsupported provider: ${provider}`)
}
}
}
Expand Down
23 changes: 21 additions & 2 deletions src/publish/restApiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -26,7 +27,7 @@ export function request<T>(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"
}

Expand All @@ -41,7 +42,7 @@ export function request<T>(url: Url, token: string | null = null, data: { [name:
return doApiRequest<T>(options, token, it => it.end(encodedData))
}

export function doApiRequest<T>(options: RequestOptions, token: string | null, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void): Promise<T> {
export function doApiRequest<T>(options: RequestOptions, token: string | null, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, redirectCount: number = 0): Promise<T> {
if (token != null) {
(<any>options.headers).authorization = token.startsWith("Basic") ? token : `token ${token}`
}
Expand All @@ -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(<any>{location: redirectUrl})
}
else {
doApiRequest(Object.assign({}, options, parseUrl(redirectUrl)), token, requestProcessor)
.then(<any>resolve)
.catch(reject)
}
return
}

let data = ""
response.setEncoding("utf8")
response.on("data", (chunk: string) => {
Expand Down
29 changes: 29 additions & 0 deletions test/src/nsisUpdaterTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(<GithubOptions>{
provider: "github",
owner: "develar",
repo: "__test_nsis_release",
}))
g.__test_resourcesPath = testResourcesPath
const updater: NsisUpdater = new NsisUpdaterClass()

const actualEvents: Array<string> = []
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)
})

0 comments on commit 9e18cb1

Please sign in to comment.