Skip to content

Commit f275831

Browse files
committed
feat(electron-updater): GitHub: Allow pre-release builds to be auto updated
Close #1391
1 parent 257a7dd commit f275831

File tree

7 files changed

+136
-39
lines changed

7 files changed

+136
-39
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"tunnel-agent": "^0.6.0",
6161
"update-notifier": "^2.1.0",
6262
"uuid-1345": "^0.99.6",
63+
"xelement": "^1.0.13",
6364
"yargs": "^7.1.0"
6465
},
6566
"devDependencies": {

packages/electron-updater/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"semver": "^5.3.0",
1919
"source-map-support": "^0.4.14",
2020
"electron-builder-http": "~0.0.0-semantic-release",
21-
"electron-is-dev": "^0.1.2"
21+
"electron-is-dev": "^0.1.2",
22+
"xelement": "^1.0.13"
2223
},
2324
"typings": "./out/electron-updater.d.ts"
2425
}

packages/electron-updater/src/AppUpdater.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export abstract class AppUpdater extends EventEmitter {
142142
client = new GenericProvider({provider: "generic", url: options})
143143
}
144144
else {
145-
client = createClient(options)
145+
client = this.createClient(options)
146146
}
147147
this.clientPromise = BluebirdPromise.resolve(client)
148148
}
@@ -184,7 +184,7 @@ export abstract class AppUpdater extends EventEmitter {
184184

185185
private async doCheckForUpdates(): Promise<UpdateCheckResult> {
186186
if (this.clientPromise == null) {
187-
this.clientPromise = this.loadUpdateConfig().then(it => createClient(it))
187+
this.clientPromise = this.loadUpdateConfig().then(it => this.createClient(it))
188188
}
189189

190190
const client = await this.clientPromise
@@ -288,41 +288,41 @@ export abstract class AppUpdater extends EventEmitter {
288288
}
289289
return requestHeaders
290290
}
291-
}
292291

293-
function createClient(data: string | PublishConfiguration) {
294-
if (typeof data === "string") {
295-
throw new Error("Please pass PublishConfiguration object")
296-
}
292+
private createClient(data: string | PublishConfiguration) {
293+
if (typeof data === "string") {
294+
throw new Error("Please pass PublishConfiguration object")
295+
}
297296

298-
const provider = (<PublishConfiguration>data).provider
299-
switch (provider) {
300-
case "github":
301-
const githubOptions = <GithubOptions>data
302-
const token = (githubOptions.private ? process.env.GH_TOKEN : null) || githubOptions.token
303-
if (token == null) {
304-
return new GitHubProvider(githubOptions)
305-
}
306-
else {
307-
return new PrivateGitHubProvider(githubOptions, token)
297+
const provider = (<PublishConfiguration>data).provider
298+
switch (provider) {
299+
case "github":
300+
const githubOptions = <GithubOptions>data
301+
const token = (githubOptions.private ? process.env.GH_TOKEN : null) || githubOptions.token
302+
if (token == null) {
303+
return new GitHubProvider(githubOptions, this)
304+
}
305+
else {
306+
return new PrivateGitHubProvider(githubOptions, token)
307+
}
308+
309+
case "s3": {
310+
const s3 = <S3Options>data
311+
return new GenericProvider({
312+
provider: "generic",
313+
url: s3Url(s3),
314+
channel: s3.channel || ""
315+
})
308316
}
309-
310-
case "s3": {
311-
const s3 = <S3Options>data
312-
return new GenericProvider({
313-
provider: "generic",
314-
url: s3Url(s3),
315-
channel: s3.channel || ""
316-
})
317-
}
318317

319-
case "generic":
320-
return new GenericProvider(<GenericServerOptions>data)
318+
case "generic":
319+
return new GenericProvider(<GenericServerOptions>data)
321320

322-
case "bintray":
323-
return new BintrayProvider(<BintrayOptions>data)
321+
case "bintray":
322+
return new BintrayProvider(<BintrayOptions>data)
324323

325-
default:
326-
throw new Error(`Unsupported provider: ${provider}`)
324+
default:
325+
throw new Error(`Unsupported provider: ${provider}`)
326+
}
327327
}
328328
}

packages/electron-updater/src/GitHubProvider.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RequestOptions } from "http"
55
import * as path from "path"
66
import { parse as parseUrl } from "url"
77
import { FileInfo, formatUrl, getChannelFilename, getCurrentPlatform, getDefaultChannelName, Provider } from "./api"
8+
import { AppUpdater } from "./AppUpdater"
89
import { validateUpdateInfo } from "./GenericProvider"
910

1011
export abstract class BaseGitHubProvider<T extends UpdateInfo> extends Provider<T> {
@@ -24,23 +25,50 @@ export abstract class BaseGitHubProvider<T extends UpdateInfo> extends Provider<
2425
}
2526

2627
export class GitHubProvider extends BaseGitHubProvider<UpdateInfo> {
27-
constructor(protected readonly options: GithubOptions) {
28+
constructor(protected readonly options: GithubOptions, private readonly updater: AppUpdater) {
2829
super(options, "github.com")
2930
}
3031

3132
async getLatestVersion(): Promise<UpdateInfo> {
3233
const basePath = this.basePath
3334
const cancellationToken = new CancellationToken()
34-
const version = await this.getLatestVersionString(basePath, cancellationToken)
35+
36+
const xElement = require("xelement")
37+
const feedXml = await request(Object.assign({
38+
path: `${basePath}.atom`,
39+
headers: Object.assign({}, this.requestHeaders, {Accept: "application/xml"})
40+
}, this.baseUrl), cancellationToken)
41+
42+
const feed = new xElement.Parse(feedXml)
43+
const latestRelease = feed.element("entry")
44+
if (latestRelease == null) {
45+
throw new Error(`No published versions on GitHub`)
46+
}
47+
48+
let version: string
49+
try {
50+
if (this.updater.allowPrerelease) {
51+
version = latestRelease.element("link").getAttr("href").match(/\/tag\/v?([^\/]+)$/)[1]
52+
}
53+
else {
54+
version = await this.getLatestVersionString(basePath, cancellationToken)
55+
}
56+
}
57+
catch (e) {
58+
throw new Error(`Cannot parse releases feed: ${e.stack || e.message},\nXML:\n${feedXml}`)
59+
}
60+
3561
let result: any
3662
const channelFile = getChannelFilename(getDefaultChannelName())
3763
const requestOptions = Object.assign({path: this.getBaseDownloadPath(version, channelFile), headers: this.requestHeaders || undefined}, this.baseUrl)
3864
try {
3965
result = await request<UpdateInfo>(requestOptions, cancellationToken)
4066
}
4167
catch (e) {
42-
if (e instanceof HttpError && e.response.statusCode === 404) {
43-
throw new Error(`Cannot find ${channelFile} in the latest release artifacts (${formatUrl(<any>requestOptions)}): ${e.stack || e.message}`)
68+
if (!this.updater.allowPrerelease) {
69+
if (e instanceof HttpError && e.response.statusCode === 404) {
70+
throw new Error(`Cannot find ${channelFile} in the latest release artifacts (${formatUrl(<any>requestOptions)}): ${e.stack || e.message}`)
71+
}
4472
}
4573
throw e
4674
}
@@ -49,13 +77,20 @@ export class GitHubProvider extends BaseGitHubProvider<UpdateInfo> {
4977
if (getCurrentPlatform() === "darwin") {
5078
result.releaseJsonUrl = `${githubUrl(this.options)}/${requestOptions.path}`
5179
}
80+
81+
if (result.releaseName == null) {
82+
result.releaseName = latestRelease.getElementValue("title")
83+
}
84+
if (result.releaseNotes == null) {
85+
result.releaseNotes = latestRelease.getElementValue("content")
86+
}
5287
return result
5388
}
5489

5590
private async getLatestVersionString(basePath: string, cancellationToken: CancellationToken): Promise<string> {
5691
const requestOptions: RequestOptions = Object.assign({
5792
path: `${basePath}/latest`,
58-
headers: Object.assign({Accept: "application/json"}, this.requestHeaders)
93+
headers: Object.assign({}, this.requestHeaders, {Accept: "application/json"})
5994
}, this.baseUrl)
6095
try {
6196
// do not use API to avoid limit

test/out/__snapshots__/nsisUpdaterTest.js.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ Object {
7777
}
7878
`;
7979

80+
exports[`file url github pre-release 1`] = `
81+
Object {
82+
"name": "TestApp-Setup-1.1.0.exe",
83+
"sha2": "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
84+
"url": "https://github.com/develar/__test_nsis_release/releases/download/v1.5.0/TestApp-Setup-1.1.0.exe",
85+
}
86+
`;
87+
88+
exports[`file url github pre-release 2`] = `
89+
Object {
90+
"path": "TestApp Setup 1.1.0.exe",
91+
"releaseName": "1.5.0",
92+
"releaseNotes": "<p>Test pre-release. qefr</p>",
93+
"sha2": "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
94+
"version": "1.5.0",
95+
}
96+
`;
97+
8098
exports[`file url github private 1`] = `
8199
Object {
82100
"headers": Object {

test/src/nsisUpdaterTest.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ test("file url github", async () => {
191191
updater.updateConfigPath = await writeUpdateConfig(<GithubOptions>{
192192
provider: "github",
193193
owner: "develar",
194-
repo: "__test_nsis_release",
194+
repo: "__test_nsis_release",
195195
})
196196

197197
const actualEvents: Array<string> = []
@@ -209,6 +209,42 @@ test("file url github", async () => {
209209
expect(actualEvents).toEqual(expectedEvents)
210210
})
211211

212+
test("file url github pre-release", async () => {
213+
const updater = new NsisUpdater(null, {
214+
getVersion: function () {
215+
return "1.6.0-beta.1"
216+
},
217+
218+
getAppPath: function () {
219+
},
220+
221+
on: function () {
222+
// ignored
223+
},
224+
})
225+
updater.updateConfigPath = await writeUpdateConfig(<GithubOptions>{
226+
provider: "github",
227+
owner: "develar",
228+
repo: "__test_nsis_release",
229+
})
230+
231+
const actualEvents: Array<string> = []
232+
const expectedEvents = ["checking-for-update", "update-available", "update-downloaded"]
233+
for (const eventName of expectedEvents) {
234+
updater.addListener(eventName, () => {
235+
actualEvents.push(eventName)
236+
})
237+
}
238+
239+
const updateCheckResult = await updater.checkForUpdates()
240+
expect(updateCheckResult.fileInfo).toMatchSnapshot()
241+
expect(updateCheckResult.versionInfo).toMatchSnapshot()
242+
243+
await assertThat(path.join(await updateCheckResult.downloadPromise)).isFile()
244+
245+
expect(actualEvents).toEqual(expectedEvents)
246+
})
247+
212248
test.skip("file url github private", async () => {
213249
const updater = new NsisUpdater()
214250
updater.updateConfigPath = await writeUpdateConfig(<GithubOptions>{

yarn.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3481,6 +3481,12 @@ xdg-basedir@^3.0.0:
34813481
version "3.0.0"
34823482
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
34833483

3484+
xelement@^1.0.13:
3485+
version "1.0.13"
3486+
resolved "https://registry.yarnpkg.com/xelement/-/xelement-1.0.13.tgz#36b724d1e6489d6aae285b06995ae5e33cec10b7"
3487+
dependencies:
3488+
sax "^1.2.1"
3489+
34843490
xml-name-validator@^2.0.1:
34853491
version "2.0.1"
34863492
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"

0 commit comments

Comments
 (0)