From 28bcf755d1fedc672bb93951bf5b8ea3ba3d46bb Mon Sep 17 00:00:00 2001 From: Thomas Bouldin Date: Tue, 16 Apr 2024 14:49:52 -0700 Subject: [PATCH] Improve getting started experience --- src/apphosting/index.ts | 51 ++++++++++++++++++++++++++++++++++------- src/utils.ts | 5 ++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts index 26ad4979aca..97bf0c12890 100644 --- a/src/apphosting/index.ts +++ b/src/apphosting/index.ts @@ -4,7 +4,7 @@ import * as repo from "./repo"; import * as poller from "../operation-poller"; import * as apphosting from "../gcp/apphosting"; import * as githubConnections from "./githubConnections"; -import { logBullet, logSuccess, logWarning } from "../utils"; +import { logBullet, logSuccess, logWarning, promiseWithSpinner, sleep } from "../utils"; import { apphostingOrigin, artifactRegistryDomain, @@ -34,6 +34,33 @@ const apphostingPollerOptions: Omit { + // Note, we do not use the helper libraries because they impose additional logic on content type and parsing. + try { + await fetch(url); + return true; + } catch (err) { + // At the time of this writing, the error code is ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE. + // I've chosen to use a regexp in an attempt to be forwards compatible with new versions of + // SSL. + const maybeNodeError = err as { cause: { code: string }}; + if (/HANDSHAKE_FAILURE/.test(maybeNodeError?.cause?.code)) { + return false; + } + return true; + } +} + +async function awaitTlsReady(url: string): Promise { + let ready; + do { + ready = await tlsReady(url); + if (!ready) { + await sleep(1000 /* ms */); + } + } while (!ready); +} + /** * Set up a new App Hosting backend. */ @@ -121,7 +148,8 @@ export async function doSetup( return; } - await orchestrateRollout(projectId, location, backendId, { + const url = `https://${backend.uri}`; + await orchestrateRollout(projectId, location, backendId, url, { source: { codebase: { branch, @@ -129,8 +157,11 @@ export async function doSetup( }, }); - logSuccess(`Successfully created backend:\n\t${backend.name}`); - logSuccess(`Your site is now deployed at:\n\thttps://${backend.uri}`); + if (!await tlsReady(url)) { + await promiseWithSpinner(() => awaitTlsReady(url), "Waiting for TLS certificate"); + } + + logSuccess(`Your site is now ready at:\n\t${url}`); } /** @@ -279,9 +310,10 @@ export async function orchestrateRollout( projectId: string, location: string, backendId: string, + url: string, buildInput: DeepOmit, ): Promise<{ rollout: Rollout; build: Build }> { - logBullet("Starting a new rollout... this may take a few minutes."); + logBullet("Starting a new rollout."); const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1); const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput); @@ -309,7 +341,7 @@ export async function orchestrateRollout( if (tries >= 5) { throw err; } - await new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep(1000); } else { throw err; } @@ -324,6 +356,10 @@ export async function orchestrateRollout( rolloutBody, ); + logSuccess( + `Your rollout has been initiated; when it completes your backend will be available at ${url}. ` + + "You may now cancel this command or let it continue running to monitor progress"); + const rolloutPoll = poller.pollOperation({ ...apphostingPollerOptions, pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, @@ -335,8 +371,7 @@ export async function orchestrateRollout( operationResourceName: buildOp.name, }); - const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]); - logSuccess("Rollout completed."); + const [rollout, build] = await promiseWithSpinner(() => Promise.all([rolloutPoll, buildPoll]), "Waiting for rollout to complete"); if (build.state !== "READY") { if (!build.buildLogsUri) { diff --git a/src/utils.ts b/src/utils.ts index 5fb40c9da52..dbfbaf865b9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -529,6 +529,11 @@ export async function promiseWithSpinner(action: () => Promise, message: s return data; } +/** Creates a promise that resolves after a given timeout. await to "sleep". */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + /** * Return a "destroy" function for a Node.js HTTP server. MUST be called on * server creation (e.g. right after `.listen`), BEFORE any connections.