-
Notifications
You must be signed in to change notification settings - Fork 918
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into bk-fix-login-message
- Loading branch information
Showing
9 changed files
with
623 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import * as _ from "lodash"; | ||
import * as api from "../api"; | ||
import * as utils from "../utils"; | ||
import { Distribution } from "./distribution"; | ||
import { FirebaseError } from "../error"; | ||
|
||
// tslint:disable-next-line:no-var-requires | ||
const pkg = require("../../package.json"); | ||
|
||
/** | ||
* Helper interface for an app that is provisioned with App Distribution | ||
*/ | ||
export interface AppDistributionApp { | ||
projectNumber: string; | ||
appId: string; | ||
platform: string; | ||
bundleId: string; | ||
contactEmail: string; | ||
} | ||
|
||
/** | ||
* Proxies HTTPS requests to the App Distribution server backend. | ||
*/ | ||
export class AppDistributionClient { | ||
static MAX_POLLING_RETRIES = 30; | ||
static POLLING_INTERVAL_MS = 1000; | ||
|
||
constructor(private readonly appId: string) {} | ||
|
||
async getApp(): Promise<AppDistributionApp> { | ||
utils.logBullet("getting app details..."); | ||
|
||
return await api.request("GET", `/v1alpha/apps/${this.appId}`, { | ||
origin: api.appDistributionOrigin, | ||
auth: true, | ||
}); | ||
} | ||
|
||
async getJwtToken(): Promise<string> { | ||
const apiResponse = await api.request("GET", `/v1alpha/apps/${this.appId}/jwt`, { | ||
auth: true, | ||
origin: api.appDistributionOrigin, | ||
}); | ||
|
||
return _.get(apiResponse, "body.token"); | ||
} | ||
|
||
async uploadDistribution(token: string, distribution: Distribution): Promise<string> { | ||
const apiResponse = await api.request("POST", "/spi/v1/jwt_distributions", { | ||
origin: api.appDistributionUploadOrigin, | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
"X-APP-DISTRO-API-CLIENT-ID": pkg.name, | ||
"X-APP-DISTRO-API-CLIENT-TYPE": distribution.platform(), | ||
"X-APP-DISTRO-API-CLIENT-VERSION": pkg.version, | ||
}, | ||
files: { | ||
file: { | ||
stream: distribution.readStream(), | ||
size: distribution.fileSize(), | ||
contentType: "multipart/form-data", | ||
}, | ||
}, | ||
}); | ||
|
||
return _.get(apiResponse, "response.headers.etag"); | ||
} | ||
|
||
async pollReleaseIdByHash(hash: string, retryCount = 0): Promise<string> { | ||
try { | ||
return await this.getReleaseIdByHash(hash); | ||
} catch (err) { | ||
if (retryCount >= AppDistributionClient.MAX_POLLING_RETRIES) { | ||
throw new FirebaseError(`failed to find the uploaded release: ${err.message}`, { exit: 1 }); | ||
} | ||
|
||
await new Promise((resolve) => | ||
setTimeout(resolve, AppDistributionClient.POLLING_INTERVAL_MS) | ||
); | ||
|
||
return this.pollReleaseIdByHash(hash, retryCount + 1); | ||
} | ||
} | ||
|
||
async getReleaseIdByHash(hash: string): Promise<string> { | ||
const apiResponse = await api.request( | ||
"GET", | ||
`/v1alpha/apps/${this.appId}/release_by_hash/${hash}`, | ||
{ | ||
origin: api.appDistributionOrigin, | ||
auth: true, | ||
} | ||
); | ||
|
||
return _.get(apiResponse, "body.release.id"); | ||
} | ||
|
||
async addReleaseNotes(releaseId: string, releaseNotes: string): Promise<void> { | ||
if (!releaseNotes) { | ||
utils.logWarning("no release notes specified, skipping"); | ||
return; | ||
} | ||
|
||
utils.logBullet("adding release notes..."); | ||
|
||
const data = { | ||
releaseNotes: { | ||
releaseNotes, | ||
}, | ||
}; | ||
|
||
try { | ||
await api.request("POST", `/v1alpha/apps/${this.appId}/releases/${releaseId}/notes`, { | ||
origin: api.appDistributionOrigin, | ||
auth: true, | ||
data, | ||
}); | ||
} catch (err) { | ||
throw new FirebaseError(`failed to add release notes with ${err.message}`, { exit: 1 }); | ||
} | ||
|
||
utils.logSuccess("added release notes successfully"); | ||
} | ||
|
||
async enableAccess( | ||
releaseId: string, | ||
emails: string[] = [], | ||
groupIds: string[] = [] | ||
): Promise<void> { | ||
if (emails.length === 0 && groupIds.length === 0) { | ||
utils.logWarning("no testers or groups specified, skipping"); | ||
return; | ||
} | ||
|
||
utils.logBullet("adding testers/groups..."); | ||
|
||
const data = { | ||
emails, | ||
groupIds, | ||
}; | ||
|
||
try { | ||
await api.request("POST", `/v1alpha/apps/${this.appId}/releases/${releaseId}/enable_access`, { | ||
origin: api.appDistributionOrigin, | ||
auth: true, | ||
data, | ||
}); | ||
} catch (err) { | ||
let errorMessage = err.message; | ||
if (_.has(err, "context.body.error")) { | ||
const errorStatus = _.get(err, "context.body.error.status"); | ||
if (errorStatus === "FAILED_PRECONDITION") { | ||
errorMessage = "invalid testers"; | ||
} else if (errorStatus === "INVALID_ARGUMENT") { | ||
errorMessage = "invalid groups"; | ||
} | ||
} | ||
throw new FirebaseError(`failed to add testers/groups: ${errorMessage}`, { exit: 1 }); | ||
} | ||
|
||
utils.logSuccess("added testers/groups successfully"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import * as fs from "fs-extra"; | ||
import { FirebaseError } from "../error"; | ||
import * as crypto from "crypto"; | ||
|
||
export enum DistributionFileType { | ||
IPA = "ipa", | ||
APK = "apk", | ||
} | ||
|
||
/** | ||
* Object representing an APK or IPa file. Used for uploading app distributions. | ||
*/ | ||
export class Distribution { | ||
private readonly fileType: DistributionFileType; | ||
|
||
constructor(private readonly path: string) { | ||
if (!path) { | ||
throw new FirebaseError("must specify a distribution file"); | ||
} | ||
|
||
const distributionType = path.split(".").pop(); | ||
if ( | ||
distributionType !== DistributionFileType.IPA && | ||
distributionType !== DistributionFileType.APK | ||
) { | ||
throw new FirebaseError("unsupported distribution file format, should be .ipa or .apk"); | ||
} | ||
|
||
if (!fs.existsSync(path)) { | ||
throw new FirebaseError( | ||
`File ${path} does not exist: verify that file points to a distribution` | ||
); | ||
} | ||
|
||
this.path = path; | ||
this.fileType = distributionType; | ||
} | ||
|
||
fileSize(): number { | ||
return fs.statSync(this.path).size; | ||
} | ||
|
||
readStream(): fs.ReadStream { | ||
return fs.createReadStream(this.path); | ||
} | ||
|
||
platform(): string { | ||
switch (this.fileType) { | ||
case DistributionFileType.IPA: | ||
return "ios"; | ||
case DistributionFileType.APK: | ||
return "android"; | ||
default: | ||
throw new FirebaseError("Unsupported distribution file format, should be .ipa or .apk"); | ||
} | ||
} | ||
|
||
async releaseHash(): Promise<string> { | ||
return new Promise<string>((resolve) => { | ||
const hash = crypto.createHash("sha1"); | ||
const stream = this.readStream(); | ||
stream.on("data", (data) => hash.update(data)); | ||
stream.on("end", () => { | ||
return resolve(hash.digest("hex")); | ||
}); | ||
}); | ||
} | ||
} |
Oops, something went wrong.