Skip to content

Commit

Permalink
Merge branch 'master' into bk-fix-login-message
Browse files Browse the repository at this point in the history
  • Loading branch information
bkendall committed Aug 26, 2019
2 parents 83a3262 + 948978b commit d0ad456
Show file tree
Hide file tree
Showing 9 changed files with 623 additions and 0 deletions.
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"marked": "^0.7.0",
"marked-terminal": "^3.3.0",
"minimatch": "^3.0.4",
"plist": "^3.0.1",
"open": "^6.3.0",
"ora": "^3.4.0",
"portfinder": "^1.0.23",
Expand Down Expand Up @@ -121,6 +122,7 @@
"@types/mocha": "^5.2.5",
"@types/nock": "^9.3.0",
"@types/node": "^10.12.0",
"@types/plist": "^3.0.1",
"@types/progress": "^2.0.3",
"@types/request": "^2.48.1",
"@types/semver": "^6.0.0",
Expand Down
8 changes: 8 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ var api = {
"https://logging.googleapis.com"
),
adminOrigin: utils.envOverride("FIREBASE_ADMIN_URL", "https://admin.firebase.com"),
appDistributionOrigin: utils.envOverride(
"FIREBASE_APP_DISTRIBUTION_URL",
"https://firebaseappdistribution.googleapis.com"
),
appDistributionUploadOrigin: utils.envOverride(
"FIREBASE_APP_DISTRIBUTION_UPLOAD_URL",
"https://appdistribution-uploads.crashlytics.com"
),
appengineOrigin: utils.envOverride("FIREBASE_APPENGINE_URL", "https://appengine.googleapis.com"),
authOrigin: utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com"),
consoleOrigin: utils.envOverride("FIREBASE_CONSOLE_URL", "https://console.firebase.google.com"),
Expand Down
163 changes: 163 additions & 0 deletions src/appdistribution/client.ts
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");
}
}
68 changes: 68 additions & 0 deletions src/appdistribution/distribution.ts
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"));
});
});
}
}
Loading

0 comments on commit d0ad456

Please sign in to comment.