Skip to content

Commit 9e18cb1

Browse files
committed
feat: GitHub publish provider
Closes #868
1 parent c5627f8 commit 9e18cb1

File tree

8 files changed

+155
-18
lines changed

8 files changed

+155
-18
lines changed

CONTRIBUTING.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,11 @@ Use one of the shared run configurations as a template and:
9797
```
9898
* Set `Environment Variables`:
9999
* `NODE_PATH` to `.`.
100-
* 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.
100+
* 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.
101+
102+
## Run Test using CLI
103+
```sh
104+
TEST_APP_TMP_DIR=/tmp/electron-builder-test NODE_PATH=. ./node_modules/.bin/ava --match="boring" test/out/nsisTest.js
105+
```
106+
107+
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.

docs/Auto Update.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
1. Install `electron-auto-updater` as app dependency.
2+
3+
2. [Confugure publish](https://github.com/electron-userland/electron-builder/wiki/Options#buildpublish).
4+
5+
3. Use `autoUpdater` from `electron-auto-updater` instead of `electron`, e.g. (ES 6):
6+
7+
```js
8+
import {autoUpdater} from "electron-auto-updater"
9+
```
10+
11+
`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.
12+
13+
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`:
14+
```js
15+
{
16+
provider: "bintray",
17+
owner: "actperepo",
18+
package: "no-versions",
19+
}
20+
```
21+
22+
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).

docs/Docker.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ Or to avoid second step, append to first command `/bin/bash -c "npm install && n
1212

1313
If you don't need to build Windows, use image `electronuserland/electron-builder:latest` (wine is not installed in this image).
1414

15-
You can use `/test.sh` to install npm dependencies and run tests.
15+
You can use `/test.sh` to install npm dependencies and run tests.
16+
17+
**NOTICE**: _Do not use Docker Toolbox on macOS._ Only [Docker for Mac](https://docs.docker.com/engine/installation/mac/#/docker-for-mac) works.

nsis-auto-updater/src/GenericProvider.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ export class GenericProvider implements Provider<UpdateInfo> {
2424
throw e
2525
}
2626

27-
if (result.sha2 == null) {
28-
throw new Error("Update info doesn't contain sha2 checksum")
29-
}
30-
if (result.path == null) {
31-
throw new Error("Update info doesn't contain file path")
32-
}
27+
validateUpdateInfo(result)
3328
return result
3429
}
3530

@@ -40,4 +35,13 @@ export class GenericProvider implements Provider<UpdateInfo> {
4035
sha2: versionInfo.sha2,
4136
}
4237
}
38+
}
39+
40+
export function validateUpdateInfo(info: UpdateInfo) {
41+
if (info.sha2 == null) {
42+
throw new Error("Update info doesn't contain sha2 checksum")
43+
}
44+
if (info.path == null) {
45+
throw new Error("Update info doesn't contain file path")
46+
}
4347
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Provider, FileInfo } from "./api"
2+
import { VersionInfo, GithubOptions, UpdateInfo } from "../../src/options/publishOptions"
3+
import { request, HttpError } from "../../src/publish/restApiRequest"
4+
import { validateUpdateInfo } from "./GenericProvider"
5+
import * as path from "path"
6+
7+
export class GitHubProvider implements Provider<VersionInfo> {
8+
constructor(private readonly options: GithubOptions) {
9+
}
10+
11+
async getLatestVersion(): Promise<UpdateInfo> {
12+
// do not use API to avoid limit
13+
const basePath = this.getBasePath()
14+
let version = (await request<Redirect>({hostname: "github.com", path: `${basePath}/latest`})).location
15+
const versionPosition = version.lastIndexOf("/") + 1
16+
try {
17+
version = version.substring(version[versionPosition] === "v" ? versionPosition + 1 : versionPosition)
18+
}
19+
catch (e) {
20+
throw new Error(`Cannot parse extract version from location "${version}": ${e.stack || e.message}`)
21+
}
22+
23+
let result: UpdateInfo | null = null
24+
try {
25+
result = await request<UpdateInfo>({hostname: "github.com", path: `https://github.com${basePath}/download/v${version}/latest.yml`})
26+
}
27+
catch (e) {
28+
if (e instanceof HttpError && e.response.statusCode === 404) {
29+
throw new Error(`Cannot find latest.yml in the latest release artifacts: ${e.stack || e.message}`)
30+
}
31+
throw e
32+
}
33+
34+
validateUpdateInfo(result)
35+
return result
36+
}
37+
38+
private getBasePath() {
39+
return `/${this.options.owner}/${this.options.repo}/releases`
40+
}
41+
42+
async getUpdateFile(versionInfo: UpdateInfo): Promise<FileInfo> {
43+
const basePath = this.getBasePath()
44+
// space is not supported on GitHub
45+
const name = path.posix.basename(versionInfo.path).replace(/ /g, "-")
46+
return {
47+
name: name,
48+
url: `https://github.com${basePath}/download/v${versionInfo.version}/${name}`,
49+
sha2: versionInfo.sha2,
50+
}
51+
}
52+
}
53+
54+
interface Redirect {
55+
readonly location: string
56+
}

nsis-auto-updater/src/NsisUpdater.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { BintrayOptions, PublishConfiguration, GithubOptions, GenericServerOptio
1111
import { readFile } from "fs-extra-p"
1212
import { safeLoad } from "js-yaml"
1313
import { GenericProvider } from "./GenericProvider"
14+
import { GitHubProvider } from "./GitHubProvider"
1415

1516
export class NsisUpdater extends EventEmitter {
1617
private setupPath: string | null
@@ -154,14 +155,11 @@ function createClient(data: string | PublishConfiguration | BintrayOptions | Git
154155
}
155156
else {
156157
const provider = (<PublishConfiguration>data).provider
157-
if (provider === "bintray") {
158-
return new BintrayProvider(<BintrayOptions>data)
159-
}
160-
else if (provider === "generic") {
161-
return new GenericProvider(<GenericServerOptions>data)
162-
}
163-
else {
164-
throw new Error(`Unsupported provider: ${provider}`)
158+
switch (provider) {
159+
case "github": return new GitHubProvider(<GithubOptions>data)
160+
case "generic": return new GenericProvider(<GenericServerOptions>data)
161+
case "bintray": return new BintrayProvider(<BintrayOptions>data)
162+
default: throw new Error(`Unsupported provider: ${provider}`)
165163
}
166164
}
167165
}

src/publish/restApiRequest.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Url } from "url"
77
import { safeLoad } from "js-yaml"
88
import _debug from "debug"
99
import Debugger = debug.Debugger
10+
import { parse as parseUrl } from "url"
1011

1112
const debug: Debugger = _debug("electron-builder")
1213

@@ -26,7 +27,7 @@ export function request<T>(url: Url, token: string | null = null, data: { [name:
2627
}
2728
}, url)
2829

29-
if (url.hostname!!.includes("github")) {
30+
if (url.hostname!!.includes("github") && !url.path!.endsWith(".yml")) {
3031
options.headers.Accept = "application/vnd.github.v3+json"
3132
}
3233

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

44-
export function doApiRequest<T>(options: RequestOptions, token: string | null, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void): Promise<T> {
45+
export function doApiRequest<T>(options: RequestOptions, token: string | null, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void, redirectCount: number = 0): Promise<T> {
4546
if (token != null) {
4647
(<any>options.headers).authorization = token.startsWith("Basic") ? token : `token ${token}`
4748
}
@@ -62,6 +63,24 @@ Please double check that your authentication token is correct. Due to security r
6263
return
6364
}
6465

66+
const redirectUrl = response.headers.location
67+
if (redirectUrl != null) {
68+
if (redirectCount > 10) {
69+
reject(new Error("Too many redirects (> 10)"))
70+
return
71+
}
72+
73+
if (options.path!.endsWith("/latest")) {
74+
resolve(<any>{location: redirectUrl})
75+
}
76+
else {
77+
doApiRequest(Object.assign({}, options, parseUrl(redirectUrl)), token, requestProcessor)
78+
.then(<any>resolve)
79+
.catch(reject)
80+
}
81+
return
82+
}
83+
6584
let data = ""
6685
response.setEncoding("utf8")
6786
response.on("data", (chunk: string) => {

test/src/nsisUpdaterTest.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TmpDir } from "out/util/tmp"
66
import { outputFile } from "fs-extra-p"
77
import { safeDump } from "js-yaml"
88
import { GenericServerOptions } from "out/options/publishOptions"
9+
import { GithubOptions } from "out/options/publishOptions"
910

1011
const NsisUpdaterClass = require("../../nsis-auto-updater/out/nsis-auto-updater/src/NsisUpdater").NsisUpdater
1112

@@ -92,5 +93,33 @@ test("file url generic", async () => {
9293
})
9394
assertThat(path.join(await updateCheckResult.downloadPromise)).isFile()
9495

96+
assertThat(actualEvents).isEqualTo(expectedEvents)
97+
})
98+
99+
test("file url github", async () => {
100+
const tmpDir = new TmpDir()
101+
const testResourcesPath = await tmpDir.getTempFile("update-config")
102+
await outputFile(path.join(testResourcesPath, "app-update.yml"), safeDump(<GithubOptions>{
103+
provider: "github",
104+
owner: "develar",
105+
repo: "__test_nsis_release",
106+
}))
107+
g.__test_resourcesPath = testResourcesPath
108+
const updater: NsisUpdater = new NsisUpdaterClass()
109+
110+
const actualEvents: Array<string> = []
111+
const expectedEvents = ["checking-for-update", "update-available", "update-downloaded"]
112+
for (let eventName of expectedEvents) {
113+
updater.addListener(eventName, () => {
114+
actualEvents.push(eventName)
115+
})
116+
}
117+
118+
const updateCheckResult = await updater.checkForUpdates()
119+
assertThat(updateCheckResult.fileInfo).hasProperties({
120+
url: "https://github.com/develar/__test_nsis_release/releases/download/v1.1.0/TestApp-Setup-1.1.0.exe"
121+
})
122+
assertThat(path.join(await updateCheckResult.downloadPromise)).isFile()
123+
95124
assertThat(actualEvents).isEqualTo(expectedEvents)
96125
})

0 commit comments

Comments
 (0)