@@ -36,6 +36,7 @@ import (
3636 "net/http"
3737 "strconv"
3838 "strings"
39+ "time"
3940
4041 "github.com/opentracing/opentracing-go"
4142)
@@ -214,6 +215,10 @@ type getCodespaceTokenResponse struct {
214215 RepositoryToken string `json:"repository_token"`
215216}
216217
218+ // ErrNotProvisioned is returned by GetCodespacesToken to indicate that the
219+ // creation of a codespace is not yet complete and that the caller should try again.
220+ var ErrNotProvisioned = errors .New ("codespace not provisioned" )
221+
217222func (a * API ) GetCodespaceToken (ctx context.Context , ownerLogin , codespaceName string ) (string , error ) {
218223 reqBody , err := json .Marshal (getCodespaceTokenRequest {true })
219224 if err != nil {
@@ -242,6 +247,10 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
242247 }
243248
244249 if resp .StatusCode != http .StatusOK {
250+ if resp .StatusCode == http .StatusUnprocessableEntity {
251+ return "" , ErrNotProvisioned
252+ }
253+
245254 return "" , jsonErrorResponse (b )
246255 }
247256
@@ -401,20 +410,83 @@ func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Rep
401410 return response .SKUs , nil
402411}
403412
404- type createCodespaceRequest struct {
413+ // CreateCodespaceParams are the required parameters for provisioning a Codespace.
414+ type CreateCodespaceParams struct {
415+ User string
416+ RepositoryID int
417+ Branch , Machine , Location string
418+ }
419+
420+ type logger interface {
421+ Print (v ... interface {}) (int , error )
422+ Println (v ... interface {}) (int , error )
423+ }
424+
425+ // CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
426+ // fails to create.
427+ func (a * API ) CreateCodespace (ctx context.Context , log logger , params * CreateCodespaceParams ) (* Codespace , error ) {
428+ codespace , err := a .startCreate (
429+ ctx , params .User , params .RepositoryID , params .Machine , params .Branch , params .Location ,
430+ )
431+ if err != errProvisioningInProgress {
432+ return codespace , err
433+ }
434+
435+ // errProvisioningInProgress indicates that codespace creation did not complete
436+ // within the GitHub API RPC time limit (10s), so it continues asynchronously.
437+ // We must poll the server to discover the outcome.
438+ ctx , cancel := context .WithTimeout (ctx , 2 * time .Minute )
439+ defer cancel ()
440+
441+ ticker := time .NewTicker (1 * time .Second )
442+ defer ticker .Stop ()
443+
444+ for {
445+ select {
446+ case <- ctx .Done ():
447+ return nil , ctx .Err ()
448+ case <- ticker .C :
449+ log .Print ("." )
450+ token , err := a .GetCodespaceToken (ctx , params .User , codespace .Name )
451+ if err != nil {
452+ if err == ErrNotProvisioned {
453+ // Do nothing. We expect this to fail until the codespace is provisioned
454+ continue
455+ }
456+
457+ return nil , fmt .Errorf ("failed to get codespace token: %w" , err )
458+ }
459+
460+ codespace , err = a .GetCodespace (ctx , token , params .User , codespace .Name )
461+ if err != nil {
462+ return nil , fmt .Errorf ("failed to get codespace: %w" , err )
463+ }
464+
465+ return codespace , nil
466+ }
467+ }
468+ }
469+
470+ type startCreateRequest struct {
405471 RepositoryID int `json:"repository_id"`
406472 Ref string `json:"ref"`
407473 Location string `json:"location"`
408474 SkuName string `json:"sku_name"`
409475}
410476
411- func (a * API ) CreateCodespace (ctx context.Context , user * User , repository * Repository , sku , branch , location string ) (* Codespace , error ) {
412- requestBody , err := json .Marshal (createCodespaceRequest {repository .ID , branch , location , sku })
477+ var errProvisioningInProgress = errors .New ("provisioning in progress" )
478+
479+ // startCreate starts the creation of a codespace.
480+ // It may return success or an error, or errProvisioningInProgress indicating that the operation
481+ // did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
482+ // must poll the server to learn the outcome.
483+ func (a * API ) startCreate (ctx context.Context , user string , repository int , sku , branch , location string ) (* Codespace , error ) {
484+ requestBody , err := json .Marshal (startCreateRequest {repository , branch , location , sku })
413485 if err != nil {
414486 return nil , fmt .Errorf ("error marshaling request: %w" , err )
415487 }
416488
417- req , err := http .NewRequest (http .MethodPost , a .githubAPI + "/vscs_internal/user/" + user . Login + "/codespaces" , bytes .NewBuffer (requestBody ))
489+ req , err := http .NewRequest (http .MethodPost , a .githubAPI + "/vscs_internal/user/" + user + "/codespaces" , bytes .NewBuffer (requestBody ))
418490 if err != nil {
419491 return nil , fmt .Errorf ("error creating request: %w" , err )
420492 }
@@ -431,8 +503,11 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
431503 return nil , fmt .Errorf ("error reading response body: %w" , err )
432504 }
433505
434- if resp .StatusCode > http .StatusAccepted {
506+ switch {
507+ case resp .StatusCode > http .StatusAccepted :
435508 return nil , jsonErrorResponse (b )
509+ case resp .StatusCode == http .StatusAccepted :
510+ return nil , errProvisioningInProgress // RPC finished before result of creation known
436511 }
437512
438513 var response Codespace
0 commit comments