@@ -36,6 +36,7 @@ import (
3636 "net/http"
3737 "strconv"
3838 "strings"
39+ "time"
3940
4041 "github.com/opentracing/opentracing-go"
4142)
@@ -208,6 +209,10 @@ type getCodespaceTokenResponse struct {
208209 RepositoryToken string `json:"repository_token"`
209210}
210211
212+ // ErrNotProvisioned is returned by GetCodespacesToken to indicate that the
213+ // creation of a codespace is not yet complete and that the caller should try again.
214+ var ErrNotProvisioned = errors .New ("codespace not provisioned" )
215+
211216func (a * API ) GetCodespaceToken (ctx context.Context , ownerLogin , codespaceName string ) (string , error ) {
212217 reqBody , err := json .Marshal (getCodespaceTokenRequest {true })
213218 if err != nil {
@@ -236,6 +241,10 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
236241 }
237242
238243 if resp .StatusCode != http .StatusOK {
244+ if resp .StatusCode == http .StatusUnprocessableEntity {
245+ return "" , ErrNotProvisioned
246+ }
247+
239248 return "" , jsonErrorResponse (b )
240249 }
241250
@@ -395,20 +404,83 @@ func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Rep
395404 return response .SKUs , nil
396405}
397406
398- type createCodespaceRequest struct {
407+ // CreateCodespaceParams are the required parameters for provisioning a Codespace.
408+ type CreateCodespaceParams struct {
409+ User string
410+ RepositoryID int
411+ Branch , Machine , Location string
412+ }
413+
414+ type logger interface {
415+ Print (v ... interface {}) (int , error )
416+ Println (v ... interface {}) (int , error )
417+ }
418+
419+ // CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
420+ // fails to create.
421+ func (a * API ) CreateCodespace (ctx context.Context , log logger , params * CreateCodespaceParams ) (* Codespace , error ) {
422+ codespace , err := a .startCreate (
423+ ctx , params .User , params .RepositoryID , params .Machine , params .Branch , params .Location ,
424+ )
425+ if err != errProvisioningInProgress {
426+ return codespace , err
427+ }
428+
429+ // errProvisioningInProgress indicates that codespace creation did not complete
430+ // within the GitHub API RPC time limit (10s), so it continues asynchronously.
431+ // We must poll the server to discover the outcome.
432+ ctx , cancel := context .WithTimeout (ctx , 2 * time .Minute )
433+ defer cancel ()
434+
435+ ticker := time .NewTicker (1 * time .Second )
436+ defer ticker .Stop ()
437+
438+ for {
439+ select {
440+ case <- ctx .Done ():
441+ return nil , ctx .Err ()
442+ case <- ticker .C :
443+ log .Print ("." )
444+ token , err := a .GetCodespaceToken (ctx , params .User , codespace .Name )
445+ if err != nil {
446+ if err == ErrNotProvisioned {
447+ // Do nothing. We expect this to fail until the codespace is provisioned
448+ continue
449+ }
450+
451+ return nil , fmt .Errorf ("failed to get codespace token: %w" , err )
452+ }
453+
454+ codespace , err = a .GetCodespace (ctx , token , params .User , codespace .Name )
455+ if err != nil {
456+ return nil , fmt .Errorf ("failed to get codespace: %w" , err )
457+ }
458+
459+ return codespace , nil
460+ }
461+ }
462+ }
463+
464+ type startCreateRequest struct {
399465 RepositoryID int `json:"repository_id"`
400466 Ref string `json:"ref"`
401467 Location string `json:"location"`
402468 SkuName string `json:"sku_name"`
403469}
404470
405- func (a * API ) CreateCodespace (ctx context.Context , user * User , repository * Repository , sku , branch , location string ) (* Codespace , error ) {
406- requestBody , err := json .Marshal (createCodespaceRequest {repository .ID , branch , location , sku })
471+ var errProvisioningInProgress = errors .New ("provisioning in progress" )
472+
473+ // startCreate starts the creation of a codespace.
474+ // It may return success or an error, or errProvisioningInProgress indicating that the operation
475+ // did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
476+ // must poll the server to learn the outcome.
477+ func (a * API ) startCreate (ctx context.Context , user string , repository int , sku , branch , location string ) (* Codespace , error ) {
478+ requestBody , err := json .Marshal (startCreateRequest {repository , branch , location , sku })
407479 if err != nil {
408480 return nil , fmt .Errorf ("error marshaling request: %w" , err )
409481 }
410482
411- req , err := http .NewRequest (http .MethodPost , githubAPI + "/vscs_internal/user/" + user . Login + "/codespaces" , bytes .NewBuffer (requestBody ))
483+ req , err := http .NewRequest (http .MethodPost , githubAPI + "/vscs_internal/user/" + user + "/codespaces" , bytes .NewBuffer (requestBody ))
412484 if err != nil {
413485 return nil , fmt .Errorf ("error creating request: %w" , err )
414486 }
@@ -425,8 +497,11 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
425497 return nil , fmt .Errorf ("error reading response body: %w" , err )
426498 }
427499
428- if resp .StatusCode > http .StatusAccepted {
500+ switch {
501+ case resp .StatusCode > http .StatusAccepted :
429502 return nil , jsonErrorResponse (b )
503+ case resp .StatusCode == http .StatusAccepted :
504+ return nil , errProvisioningInProgress // RPC finished before result of creation known
430505 }
431506
432507 var response Codespace
0 commit comments