Skip to content

Commit

Permalink
Create rollout from branch or git commit (#7687)
Browse files Browse the repository at this point in the history
* fix create rollouts

* fix create rollout flow

* update flow to match api proposal

* add unit tests for rollouts command
  • Loading branch information
blidd-google authored Oct 7, 2024
1 parent 158c2b5 commit f350d60
Show file tree
Hide file tree
Showing 8 changed files with 671 additions and 105 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Add apphosting:rollouts:create CLI command. (#7687)
- Updated emulator UI to version 1.14.0, which adds support for SDK defined extensions.
- Added emulator support for SDK defined extensions.
- Fixed various trigger handling issues in the Functions emualtor, including an issue where Eventarc functions would not be emulated correctly after a reload.
Expand Down
68 changes: 66 additions & 2 deletions src/apphosting/githubConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ import * as utils from "../utils";
import { FirebaseError } from "../error";
import { promptOnce } from "../prompt";
import { getProjectNumber } from "../getProjectNumber";
import { apphostingGitHubAppInstallationURL, developerConnectOrigin } from "../api";
import {
apphostingGitHubAppInstallationURL,
developerConnectOrigin,
githubApiOrigin,
} from "../api";

import * as fuzzy from "fuzzy";
import * as inquirer from "inquirer";
import { Client } from "../apiv2";

const githubApiClient = new Client({ urlPrefix: githubApiOrigin(), auth: false });

export interface GitHubBranchInfo {
commit: GitHubCommitInfo;
}

export interface GitHubCommitInfo {
sha: string;
commit: GitHubCommit;
}

interface GitHubCommit {
message: string;
}

interface ConnectionNameParts {
projectId: string;
Expand Down Expand Up @@ -212,6 +232,9 @@ async function manageInstallation(connection: devConnect.Connection): Promise<vo
});
}

/**
* Gets the oldest matching Dev Connect connection resource for a GitHub app installation.
*/
export async function getConnectionForInstallation(
projectId: string,
location: string,
Expand Down Expand Up @@ -240,6 +263,9 @@ export async function getConnectionForInstallation(
return connectionsMatchingInstallation[0];
}

/**
* Prompts the user to select which GitHub account to install the GitHub app.
*/
export async function promptGitHubInstallation(
projectId: string,
location: string,
Expand Down Expand Up @@ -383,7 +409,7 @@ async function promptCloneUri(
* Prompts the user for a GitHub branch and validates that the given branch
* actually exists. User is re-prompted until they enter a valid branch.
*/
export async function promptGitHubBranch(repoLink: devConnect.GitRepositoryLink) {
export async function promptGitHubBranch(repoLink: devConnect.GitRepositoryLink): Promise<string> {
const branches = await devConnect.listAllBranches(repoLink.name);
while (true) {
const branch = await promptOnce({
Expand Down Expand Up @@ -579,3 +605,41 @@ export async function fetchRepositoryCloneUris(

return cloneUris;
}

/**
* Gets the details of a GitHub branch from the GitHub REST API.
*/
export async function getGitHubBranch(
owner: string,
repo: string,
branch: string,
readToken: string,
): Promise<GitHubBranchInfo> {
const headers = { Authorization: `Bearer ${readToken}`, "User-Agent": "Firebase CLI" };
const { body } = await githubApiClient.get<GitHubBranchInfo>(
`/repos/${owner}/${repo}/branches/${branch}`,
{
headers,
},
);
return body;
}

/**
* Gets the details of a GitHub commit from the GitHub REST API.
*/
export async function getGitHubCommit(
owner: string,
repo: string,
ref: string,
readToken: string,
): Promise<GitHubCommitInfo> {
const headers = { Authorization: `Bearer ${readToken}`, "User-Agent": "Firebase CLI" };
const { body } = await githubApiClient.get<GitHubCommitInfo>(
`/repos/${owner}/${repo}/commits/${ref}`,
{
headers,
},
);
return body;
}
98 changes: 13 additions & 85 deletions src/apphosting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
iamOrigin,
secretManagerOrigin,
} from "../api";
import { Backend, BackendOutputOnlyFields, API_VERSION, Build, Rollout } from "../gcp/apphosting";
import { Backend, BackendOutputOnlyFields, API_VERSION } from "../gcp/apphosting";
import { addServiceAccountToRoles } from "../gcp/resourceManager";
import * as iam from "../gcp/iam";
import { FirebaseError } from "../error";
Expand All @@ -26,6 +26,7 @@ import { webApps } from "./app";
import { GitRepositoryLink } from "../gcp/devConnect";
import * as ora from "ora";
import fetch from "node-fetch";
import { orchestrateRollout } from "./rollout";

const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";

Expand Down Expand Up @@ -167,12 +168,18 @@ export async function doSetup(
const createRolloutSpinner = ora(
"Starting a new rollout; this may take a few minutes. It's safe to exit now.",
).start();
await orchestrateRollout(projectId, location, backendId, {
source: {
codebase: {
branch,
await orchestrateRollout({
projectId,
location,
backendId,
buildInput: {
source: {
codebase: {
branch,
},
},
},
isFirstRollout: true,
});
createRolloutSpinner.succeed("Rollout complete");
if (!(await tlsReady(url))) {
Expand Down Expand Up @@ -340,85 +347,6 @@ export async function setDefaultTrafficPolicy(
});
}

/**
* Creates a new build and rollout and polls both to completion.
*/
export async function orchestrateRollout(
projectId: string,
location: string,
backendId: string,
buildInput: DeepOmit<Build, apphosting.BuildOutputOnlyFields | "name">,
): Promise<{ rollout: Rollout; build: Build }> {
const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);

const rolloutBody = {
build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`,
};

let tries = 0;
let done = false;
while (!done) {
tries++;
try {
const validateOnly = true;
await apphosting.createRollout(
projectId,
location,
backendId,
buildId,
rolloutBody,
validateOnly,
);
done = true;
} catch (err: unknown) {
if (err instanceof FirebaseError && err.status === 400) {
if (tries >= 5) {
throw err;
}
await sleep(1000);
} else {
throw err;
}
}
}

const rolloutOp = await apphosting.createRollout(
projectId,
location,
backendId,
buildId,
rolloutBody,
);

const rolloutPoll = poller.pollOperation<Rollout>({
...apphostingPollerOptions,
pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`,
operationResourceName: rolloutOp.name,
});
const buildPoll = poller.pollOperation<Build>({
...apphostingPollerOptions,
pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`,
operationResourceName: buildOp.name,
});

const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);

if (build.state !== "READY") {
if (!build.buildLogsUri) {
throw new FirebaseError(
"Failed to build your app, but failed to get build logs as well. " +
"This is an internal error and should be reported",
);
}
throw new FirebaseError(
`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`,
{ children: [build.error] },
);
}
return { rollout, build };
}

/**
* Deletes the given backend. Polls till completion.
*/
Expand All @@ -440,7 +368,7 @@ export async function deleteBackendAndPoll(
*/
export async function promptLocation(
projectId: string,
prompt: string = "Please select a location:",
prompt = "Please select a location:",
): Promise<string> {
const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
if (allowedLocations.length === 1) {
Expand Down
Loading

0 comments on commit f350d60

Please sign in to comment.