Skip to content

Commit

Permalink
feat: upgrade keygen integration to v1.1 (electron-userland#6941)
Browse files Browse the repository at this point in the history
  • Loading branch information
ezekg authored Jun 17, 2022
1 parent 7bbf730 commit 14503ce
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 46 deletions.
6 changes: 6 additions & 0 deletions .changeset/seven-frogs-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"app-builder-lib": minor
"electron-updater": minor
---

Upgrade Keygen publisher/updater integration to API version v1.1.
218 changes: 177 additions & 41 deletions packages/app-builder-lib/src/publish/KeygenPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,77 @@ import { KeygenOptions } from "builder-util-runtime/out/publishOptions"
import { configureRequestOptions, HttpExecutor, parseJson } from "builder-util-runtime"
import { getCompleteExtname } from "../util/filename"

type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}

export interface KeygenError {
title: string
detail: string
code: string
}

export interface KeygenRelease {
id: string
type: "releases"
attributes: {
name: string | null
description: string | null
channel: "stable" | "rc" | "beta" | "alpha" | "dev"
status: "DRAFT" | "PUBLISHED" | "YANKED"
tag: string
version: string
semver: {
major: number
minor: number
patch: number
prerelease: string | null
build: string | null
}
metadata: { [s: string]: any }
created: string
updated: string
yanked: string | null
}
relationships: {
account: {
data: { type: "accounts"; id: string }
}
product: {
data: { type: "products"; id: string }
}
}
}

export interface KeygenArtifact {
id: string
type: "artifacts"
attributes: {
filename: string
filetype: string | null
filesize: number | null
platform: string | null
arch: string | null
signature: string | null
checksum: string | null
status: "WAITING" | "UPLOADED" | "FAILED" | "YANKED"
metadata: { [s: string]: any }
created: string
updated: string
}
relationships: {
account: {
data: { type: "accounts"; id: string }
}
release: {
data: { type: "releases"; id: string }
}
}
links: {
redirect: string
}
}

export class KeygenPublisher extends HttpPublisher {
readonly providerName = "keygen"
readonly hostname = "api.keygen.sh"
Expand All @@ -26,7 +97,7 @@ export class KeygenPublisher extends HttpPublisher {
this.info = info
this.auth = `Bearer ${token.trim()}`
this.version = version
this.basePath = `/v1/accounts/${this.info.account}/releases`
this.basePath = `/v1/accounts/${this.info.account}`
}

protected doUpload(
Expand All @@ -36,78 +107,143 @@ export class KeygenPublisher extends HttpPublisher {
requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_file: string
): Promise<any> {
): Promise<string> {
return HttpExecutor.retryOnServerError(async () => {
const { data, errors } = await this.upsertRelease(fileName, dataLength)
const { data, errors } = await this.getOrCreateRelease()
if (errors) {
throw new Error(`Keygen - Upserting release returned errors: ${JSON.stringify(errors)}`)
throw new Error(`Keygen - Creating release returned errors: ${JSON.stringify(errors)}`)
}
const releaseId = data?.id
if (!releaseId) {
log.warn({ file: fileName, reason: "UUID doesn't exist and was not created" }, "upserting release failed")
throw new Error(`Keygen - Upserting release returned no UUID: ${JSON.stringify(data)}`)
}
await this.uploadArtifact(releaseId, dataLength, requestProcessor)
return releaseId

await this.uploadArtifact(data!.id, fileName, dataLength, requestProcessor)

return data!.id
})
}

private async uploadArtifact(releaseId: any, dataLength: number, requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void) {
private async uploadArtifact(
releaseId: any,
fileName: string,
dataLength: number,
requestProcessor: (request: ClientRequest, reject: (error: Error) => void) => void
): Promise<void> {
const { data, errors } = await this.createArtifact(releaseId, fileName, dataLength)
if (errors) {
throw new Error(`Keygen - Creating artifact returned errors: ${JSON.stringify(errors)}`)
}

// Follow the redirect and upload directly to S3-equivalent storage provider
const url = new URL(data!.links.redirect)
const upload: RequestOptions = {
hostname: url.hostname,
path: url.pathname + url.search,
headers: {
"Content-Length": dataLength,
},
}

await httpExecutor.doApiRequest(configureRequestOptions(upload, null, "PUT"), this.context.cancellationToken, requestProcessor)
}

private async createArtifact(releaseId: any, fileName: string, dataLength: number): Promise<{ data?: KeygenArtifact; errors?: KeygenError[] }> {
const upload: RequestOptions = {
hostname: this.hostname,
path: `${this.basePath}/${releaseId}/artifact`,
path: `${this.basePath}/artifacts`,
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
"Content-Length": dataLength,
"Keygen-Version": "1.0",
"Keygen-Version": "1.1",
Prefer: "no-redirect",
},
}

const data: RecursivePartial<KeygenArtifact> = {
type: "artifacts",
attributes: {
filename: fileName,
filetype: getCompleteExtname(fileName),
filesize: dataLength,
platform: this.info.platform,
},
relationships: {
release: {
data: {
type: "releases",
id: releaseId,
},
},
},
}
await httpExecutor.doApiRequest(configureRequestOptions(upload, this.auth, "PUT"), this.context.cancellationToken, requestProcessor)

log.debug({ data: JSON.stringify(data) }, "Keygen create artifact")

return parseJson(httpExecutor.request(configureRequestOptions(upload, this.auth, "POST"), this.context.cancellationToken, { data }))
}

private async upsertRelease(fileName: string, dataLength: number): Promise<{ data: any; errors: any }> {
private async getOrCreateRelease(): Promise<{ data?: KeygenRelease; errors?: KeygenError[] }> {
try {
return await this.getRelease()
} catch (e) {
if (e.statusCode === 404) {
return this.createRelease()
}

throw e
}
}

private async getRelease(): Promise<{ data?: KeygenRelease; errors?: KeygenError[] }> {
const req: RequestOptions = {
hostname: this.hostname,
method: "PUT",
path: this.basePath,
path: `${this.basePath}/releases/${this.version}`,
headers: {
Accept: "application/vnd.api+json",
"Keygen-Version": "1.1",
},
}

return parseJson(httpExecutor.request(configureRequestOptions(req, this.auth, "GET"), this.context.cancellationToken, null))
}

private async createRelease(): Promise<{ data?: KeygenRelease; errors?: KeygenError[] }> {
const req: RequestOptions = {
hostname: this.hostname,
path: `${this.basePath}/releases`,
headers: {
"Content-Type": "application/vnd.api+json",
Accept: "application/vnd.api+json",
"Keygen-Version": "1.0",
"Keygen-Version": "1.1",
},
}
const data = {
data: {
type: "release",
attributes: {
filename: fileName,
filetype: getCompleteExtname(fileName),
filesize: dataLength,
version: this.version,
platform: this.info.platform,
channel: this.info.channel || "stable",
},
relationships: {
product: {
data: {
type: "product",
id: this.info.product,
},

const data: RecursivePartial<KeygenRelease> = {
type: "releases",
attributes: {
version: this.version,
channel: this.info.channel || "stable",
status: "PUBLISHED",
},
relationships: {
product: {
data: {
type: "products",
id: this.info.product,
},
},
},
}
log.debug({ data: JSON.stringify(data) }, "Keygen upsert release")
return parseJson(httpExecutor.request(configureRequestOptions(req, this.auth, "PUT"), this.context.cancellationToken, data))

log.debug({ data: JSON.stringify(data) }, "Keygen create release")

return parseJson(httpExecutor.request(configureRequestOptions(req, this.auth, "POST"), this.context.cancellationToken, { data }))
}

async deleteRelease(releaseId: string): Promise<void> {
const req: RequestOptions = {
hostname: this.hostname,
path: `${this.basePath}/${releaseId}`,
path: `${this.basePath}/releases/${releaseId}`,
headers: {
Accept: "application/vnd.api+json",
"Keygen-Version": "1.0",
"Keygen-Version": "1.1",
},
}
await httpExecutor.request(configureRequestOptions(req, this.auth, "DELETE"), this.context.cancellationToken)
Expand Down
2 changes: 1 addition & 1 deletion packages/electron-updater/src/providers/KeygenProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class KeygenProvider extends Provider<UpdateInfo> {
channelUrl,
{
Accept: "application/vnd.api+json",
"Keygen-Version": "1.0",
"Keygen-Version": "1.1",
},
cancellationToken
)
Expand Down
4 changes: 2 additions & 2 deletions test/src/ArtifactPublisherTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ test.ifEnv(process.env.KEYGEN_TOKEN)("Keygen upload", async () => {
{
provider: "keygen",
// electron-builder-test
product: "43981278-96e7-47de-b8c2-98d59987206b",
account: "cdecda36-3ef0-483e-ad88-97e7970f3149",
product: process.env.KEYGEN_PRODUCT || "43981278-96e7-47de-b8c2-98d59987206b",
account: process.env.KEYGEN_ACCOUNT || "cdecda36-3ef0-483e-ad88-97e7970f3149",
platform: Platform.MAC.name,
} as KeygenOptions,
versionNumber()
Expand Down
4 changes: 2 additions & 2 deletions test/src/updater/nsisUpdaterTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ test.ifEnv(process.env.KEYGEN_TOKEN)("file url keygen", async () => {
updater.addAuthHeader(`Bearer ${process.env.KEYGEN_TOKEN}`)
updater.updateConfigPath = await writeUpdateConfig<KeygenOptions>({
provider: "keygen",
product: "43981278-96e7-47de-b8c2-98d59987206b",
account: "cdecda36-3ef0-483e-ad88-97e7970f3149",
product: process.env.KEYGEN_PRODUCT || "43981278-96e7-47de-b8c2-98d59987206b",
account: process.env.KEYGEN_ACCOUNT || "cdecda36-3ef0-483e-ad88-97e7970f3149",
})
await validateDownload(updater)
})
Expand Down

0 comments on commit 14503ce

Please sign in to comment.