Skip to content

Commit a7ba250

Browse files
committed
rename maybeLinkGitHub: boolean to validateGitHubLink: void, more error throwing, check local repo against configured remote repo, just repo and branch, not yet refs
1 parent 06b52f7 commit a7ba250

File tree

2 files changed

+81
-27
lines changed

2 files changed

+81
-27
lines changed

src/deploy.ts

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export function formatGitUrl(url: string) {
4242
return new URL(url).pathname.slice(1).replace(/\.git$/, "");
4343
}
4444

45+
function settingsUrl(deployTarget: DeployTargetInfo) {
46+
if (deployTarget.create) {
47+
throw new Error("Incorrect deploy target state");
48+
}
49+
return `${OBSERVABLE_UI_ORIGIN}projects/@${deployTarget.workspace.login}/${deployTarget.project.slug}`;
50+
}
51+
4552
export interface DeployOptions {
4653
config: Config;
4754
deployConfigPath: string | undefined;
@@ -223,9 +230,7 @@ class Deployer {
223230
});
224231
if (latestCreatedDeployId !== deployTarget.project.latestCreatedDeployId) {
225232
spinner.stop(
226-
`Deploy started. Watch logs: ${process.env["OBSERVABLE_ORIGIN"] ?? "https://observablehq.com"}/projects/@${
227-
deployTarget.workspace.login
228-
}/${deployTarget.project.slug}/deploys/${latestCreatedDeployId}`
233+
`Deploy started. Watch logs: ${link(`${settingsUrl(deployTarget)}/deploys/${latestCreatedDeployId}`)}`
229234
);
230235
// latestCreatedDeployId is initially null for a new project, but once
231236
// it changes to a string it can never change back; since we know it has
@@ -236,63 +241,104 @@ class Deployer {
236241
}
237242
}
238243

239-
private async maybeLinkGitHub(deployTarget: DeployTargetInfo): Promise<boolean> {
244+
// Throws error if local and remote GitHub repos don’t match or are invalid
245+
private async validateGitHubLink(deployTarget: DeployTargetInfo): Promise<void> {
240246
if (deployTarget.create) {
241247
throw new Error("Incorrect deploy target state");
242248
}
243249
// We only support cloud builds from the root directory so this ignores this.deployOptions.config.root
244250
const isGit = existsSync(".git");
245-
if (!isGit) throw new CliError("Not at root of a git repository; cannot enable continuous deployment.");
251+
if (!isGit) throw new CliError("Not at root of a git repository.");
246252
const remotes = (await promisify(exec)("git remote -v", {cwd: this.deployOptions.config.root})).stdout
247253
.split("\n")
248254
.filter((d) => d)
249255
.map((d) => d.split(/\s/g));
250256
const gitHub = remotes.find(([, url]) => url.startsWith("https://github.com/"));
251-
if (!gitHub) throw new CliError("No GitHub remote found; cannot enable continuous deployment.");
257+
if (!gitHub) throw new CliError("No GitHub remote found.");
252258
// TODO: validate "Your branch is up to date" & "nothing to commit, working tree clean"
253259

254-
// TODO allow setting this from CLI?
255260
if (!deployTarget.project.build_environment_id) throw new CliError("No build environment configured.");
261+
// TODO: allow setting build environment from CLI
256262

257-
// can do cloud build
258-
// TODO: validate local/remote refs match & we can access repo
259-
if (deployTarget.project.source) return true;
260-
261-
// Interactively try to link repository
262-
if (!this.effects.isTty) return false;
263263
const [ownerName, repoName] = formatGitUrl(gitHub[1]).split("/");
264-
// Get current branch
265264
const branch = (await promisify(exec)("git rev-parse --abbrev-ref HEAD", {cwd: this.deployOptions.config.root}))
266265
.stdout;
267-
let authedRepo = await this.apiClient.getGitHubRepository(ownerName, repoName);
268-
if (!authedRepo) {
266+
267+
let localRepo = await this.apiClient.getGitHubRepository({ownerName, repoName});
268+
269+
// If a source repository has already been configured, check that it’s
270+
// accessible and matches the local repository and branch
271+
if (deployTarget.project.source) {
272+
if (localRepo && deployTarget.project.source.provider_id !== localRepo.provider_id) {
273+
throw new CliError(
274+
`Configured repository does not match local repository; check build settings on ${link(
275+
`${settingsUrl(deployTarget)}/settings`
276+
)}`
277+
);
278+
}
279+
if (localRepo && deployTarget.project.source.branch !== branch) {
280+
throw new CliError(
281+
`Configured branch does not match local branch; check build settings on ${link(
282+
`${settingsUrl(deployTarget)}/settings`
283+
)}`
284+
);
285+
}
286+
// TODO: validate local/remote refs match
287+
const remoteAuthedRepo = await this.apiClient.getGitHubRepository({
288+
providerId: deployTarget.project.source.provider_id
289+
});
290+
if (!remoteAuthedRepo) {
291+
console.log(deployTarget.project.source.provider_id, remoteAuthedRepo);
292+
throw new CliError(
293+
`Cannot access configured repository; check build settings on ${link(
294+
`${settingsUrl(deployTarget)}/settings`
295+
)}`
296+
);
297+
}
298+
299+
// Configured repo is OK; proceed
300+
return;
301+
}
302+
303+
if (!localRepo) {
304+
if (!this.effects.isTty)
305+
throw new CliError(
306+
"Cannot access repository for continuous deployment and cannot request access in non-interactive mode"
307+
);
308+
269309
// Repo is not authorized; link to auth page and poll for auth
270310
const authUrl = new URL("/auth-github", OBSERVABLE_UI_ORIGIN);
271311
authUrl.searchParams.set("owner", ownerName);
272312
authUrl.searchParams.set("repo", repoName);
273313
this.effects.clack.log.info(`Authorize Observable to access the ${bold(repoName)} repository: ${link(authUrl)}`);
314+
274315
const spinner = this.effects.clack.spinner();
275316
spinner.start("Waiting for repository to be authorized");
276317
const pollExpiration = Date.now() + DEPLOY_POLL_MAX_MS;
277-
while (!authedRepo) {
318+
while (!localRepo) {
278319
await new Promise((resolve) => setTimeout(resolve, 2000));
279320
if (Date.now() > pollExpiration) {
280321
spinner.stop("Waiting for repository to be authorized timed out.");
281322
throw new CliError("Repository authorization failed");
282323
}
283-
authedRepo = await this.apiClient.getGitHubRepository(ownerName, repoName);
284-
if (authedRepo) spinner.stop("Repository authorized.");
324+
localRepo = await this.apiClient.getGitHubRepository({ownerName, repoName});
325+
if (localRepo) spinner.stop("Repository authorized.");
285326
}
286327
}
328+
287329
const response = await this.apiClient.postProjectEnvironment(deployTarget.project.id, {
288330
source: {
289-
provider: authedRepo.provider,
290-
provider_id: authedRepo.provider_id,
291-
url: authedRepo.url,
331+
provider: localRepo.provider,
332+
provider_id: localRepo.provider_id,
333+
url: localRepo.url,
292334
branch
293335
}
294336
});
295-
return !!response;
337+
338+
if (!response) throw new CliError("Setting source repository for continuous deployment failed");
339+
340+
// Configured repo is OK; proceed
341+
return;
296342
}
297343

298344
private async startNewDeploy(): Promise<GetDeployResponse> {
@@ -514,8 +560,7 @@ class Deployer {
514560
continuousDeployment = enable;
515561
}
516562

517-
// Disables continuous deployment if there’s no env/source & we can’t link GitHub
518-
if (continuousDeployment) await this.maybeLinkGitHub(deployTarget);
563+
if (continuousDeployment) await this.validateGitHubLink(deployTarget);
519564

520565
const newDeployConfig = {
521566
projectId: deployTarget.project.id,

src/observableApiClient.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,20 @@ export class ObservableApiClient {
126126
return await this._fetch<GetProjectResponse>(url, {method: "GET"});
127127
}
128128

129-
async getGitHubRepository(ownerName, repoName): Promise<GetGitHubRepositoryResponse | null> {
130-
const url = new URL(`/cli/github/repository?owner=${ownerName}&repo=${repoName}`, this._apiOrigin);
129+
async getGitHubRepository(
130+
props: {ownerName: string; repoName: string} | {providerId: string}
131+
): Promise<GetGitHubRepositoryResponse | null> {
132+
let url: URL;
133+
if ("providerId" in props) {
134+
url = new URL(`/cli/github/repository?provider_id=${props.providerId}`, this._apiOrigin);
135+
} else {
136+
url = new URL(`/cli/github/repository?owner=${props.ownerName}&repo=${props.repoName}`, this._apiOrigin);
137+
}
131138
try {
132139
return await this._fetch<GetGitHubRepositoryResponse>(url, {method: "GET"});
133140
} catch (err) {
141+
// TODO: err.details.errors may be [{code: "NO_GITHUB_TOKEN"}] or [{code: "NO_REPO_ACCESS"}],
142+
// which could be handled separately
134143
return null;
135144
}
136145
}

0 commit comments

Comments
 (0)