@@ -42,6 +42,13 @@ export function formatGitUrl(url: string) {
42
42
return new URL ( url ) . pathname . slice ( 1 ) . replace ( / \. g i t $ / , "" ) ;
43
43
}
44
44
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
+
45
52
export interface DeployOptions {
46
53
config : Config ;
47
54
deployConfigPath : string | undefined ;
@@ -223,9 +230,7 @@ class Deployer {
223
230
} ) ;
224
231
if ( latestCreatedDeployId !== deployTarget . project . latestCreatedDeployId ) {
225
232
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 } ` ) } `
229
234
) ;
230
235
// latestCreatedDeployId is initially null for a new project, but once
231
236
// it changes to a string it can never change back; since we know it has
@@ -236,63 +241,104 @@ class Deployer {
236
241
}
237
242
}
238
243
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 > {
240
246
if ( deployTarget . create ) {
241
247
throw new Error ( "Incorrect deploy target state" ) ;
242
248
}
243
249
// We only support cloud builds from the root directory so this ignores this.deployOptions.config.root
244
250
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." ) ;
246
252
const remotes = ( await promisify ( exec ) ( "git remote -v" , { cwd : this . deployOptions . config . root } ) ) . stdout
247
253
. split ( "\n" )
248
254
. filter ( ( d ) => d )
249
255
. map ( ( d ) => d . split ( / \s / g) ) ;
250
256
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." ) ;
252
258
// TODO: validate "Your branch is up to date" & "nothing to commit, working tree clean"
253
259
254
- // TODO allow setting this from CLI?
255
260
if ( ! deployTarget . project . build_environment_id ) throw new CliError ( "No build environment configured." ) ;
261
+ // TODO: allow setting build environment from CLI
256
262
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 ;
263
263
const [ ownerName , repoName ] = formatGitUrl ( gitHub [ 1 ] ) . split ( "/" ) ;
264
- // Get current branch
265
264
const branch = ( await promisify ( exec ) ( "git rev-parse --abbrev-ref HEAD" , { cwd : this . deployOptions . config . root } ) )
266
265
. 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
+
269
309
// Repo is not authorized; link to auth page and poll for auth
270
310
const authUrl = new URL ( "/auth-github" , OBSERVABLE_UI_ORIGIN ) ;
271
311
authUrl . searchParams . set ( "owner" , ownerName ) ;
272
312
authUrl . searchParams . set ( "repo" , repoName ) ;
273
313
this . effects . clack . log . info ( `Authorize Observable to access the ${ bold ( repoName ) } repository: ${ link ( authUrl ) } ` ) ;
314
+
274
315
const spinner = this . effects . clack . spinner ( ) ;
275
316
spinner . start ( "Waiting for repository to be authorized" ) ;
276
317
const pollExpiration = Date . now ( ) + DEPLOY_POLL_MAX_MS ;
277
- while ( ! authedRepo ) {
318
+ while ( ! localRepo ) {
278
319
await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
279
320
if ( Date . now ( ) > pollExpiration ) {
280
321
spinner . stop ( "Waiting for repository to be authorized timed out." ) ;
281
322
throw new CliError ( "Repository authorization failed" ) ;
282
323
}
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." ) ;
285
326
}
286
327
}
328
+
287
329
const response = await this . apiClient . postProjectEnvironment ( deployTarget . project . id , {
288
330
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 ,
292
334
branch
293
335
}
294
336
} ) ;
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 ;
296
342
}
297
343
298
344
private async startNewDeploy ( ) : Promise < GetDeployResponse > {
@@ -514,8 +560,7 @@ class Deployer {
514
560
continuousDeployment = enable ;
515
561
}
516
562
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 ) ;
519
564
520
565
const newDeployConfig = {
521
566
projectId : deployTarget . project . id ,
0 commit comments