diff --git a/cmd/internal/app_store_connect/client.go b/cmd/internal/app_store_connect/client.go new file mode 100644 index 00000000..bdc76076 --- /dev/null +++ b/cmd/internal/app_store_connect/client.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + _ "unsafe" + + "github.com/cidertool/asc-go/asc" +) + +type Client struct { + *asc.Client +} + +func (c *Client) UpdateBuildForAppStoreVersion(ctx context.Context, id string, buildID *string) (*asc.Response, error) { + linkage := newRelationshipDeclaration(buildID, "builds") + url := fmt.Sprintf("appStoreVersions/%s/relationships/build", id) + return c.patch(ctx, url, newRequestBody(linkage), nil) +} + +func newRelationshipDeclaration(id *string, relationshipType string) *asc.RelationshipData { + if id == nil { + return nil + } + + return &asc.RelationshipData{ + ID: *id, + Type: relationshipType, + } +} diff --git a/cmd/internal/app_store_connect/client_linkname.go b/cmd/internal/app_store_connect/client_linkname.go new file mode 100644 index 00000000..7ccfc4fe --- /dev/null +++ b/cmd/internal/app_store_connect/client_linkname.go @@ -0,0 +1,140 @@ +package main + +import ( + "context" + "net/http" + "net/url" + "reflect" + _ "unsafe" + + "github.com/cidertool/asc-go/asc" + "github.com/google/go-querystring/query" +) + +func (c *Client) newRequest(ctx context.Context, method string, path string, body *requestBody, options ...requestOption) (*http.Request, error) { + return clientNewRequest(c.Client, ctx, method, path, body, options...) +} + +//go:linkname clientNewRequest github.com/cidertool/asc-go/asc.(*Client).newRequest +func clientNewRequest(c *asc.Client, ctx context.Context, method string, path string, body *requestBody, options ...requestOption) (*http.Request, error) + +func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*asc.Response, error) { + return clientDo(c.Client, ctx, req, v) +} + +//go:linkname clientDo github.com/cidertool/asc-go/asc.(*Client).do +func clientDo(c *asc.Client, ctx context.Context, req *http.Request, v interface{}) (*asc.Response, error) + +// get sends a GET request to the API as configured. +func (c *Client) get(ctx context.Context, url string, query interface{}, v interface{}, options ...requestOption) (*asc.Response, error) { + var err error + if query != nil { + url, err = appendingQueryOptions(url, query) + if err != nil { + return nil, err + } + } + + req, err := c.newRequest(ctx, "GET", url, nil, options...) + if err != nil { + return nil, err + } + + resp, err := c.do(ctx, req, v) + if err != nil { + return resp, err + } + + return resp, err +} + +// post sends a POST request to the API as configured. +func (c *Client) post(ctx context.Context, url string, body *requestBody, v interface{}) (*asc.Response, error) { + req, err := c.newRequest(ctx, "POST", url, body, withContentType("application/json")) + if err != nil { + return nil, err + } + + resp, err := c.do(ctx, req, v) + if err != nil { + return resp, err + } + + return resp, err +} + +// patch sends a PATCH request to the API as configured. +func (c *Client) patch(ctx context.Context, url string, body *requestBody, v interface{}) (*asc.Response, error) { + req, err := c.newRequest(ctx, "PATCH", url, body, withContentType("application/json")) + if err != nil { + return nil, err + } + + resp, err := c.do(ctx, req, v) + if err != nil { + return resp, err + } + + return resp, err +} + +// delete sends a DELETE request to the API as configured. +func (c *Client) delete(ctx context.Context, url string, body *requestBody) (*asc.Response, error) { + req, err := c.newRequest(ctx, "DELETE", url, body, withContentType("application/json")) + if err != nil { + return nil, err + } + + return c.do(ctx, req, nil) +} + +// request is a common structure for a request body sent to the API. +type requestBody struct { + Data interface{} `json:"data"` + Included interface{} `json:"included,omitempty"` +} + +func newRequestBody(data interface{}) *requestBody { + return newRequestBodyWithIncluded(data, nil) +} + +func newRequestBodyWithIncluded(data interface{}, included interface{}) *requestBody { + return &requestBody{Data: data, Included: included} +} + +type requestOption func(*http.Request) + +func withAccept(typ string) requestOption { + return func(req *http.Request) { + req.Header.Set("Accept", typ) + } +} + +func withContentType(typ string) requestOption { + return func(req *http.Request) { + req.Header.Set("Content-Type", typ) + } +} + +// AddOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func appendingQueryOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + u.RawQuery = qs.Encode() + + return u.String(), nil +} diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go index 32e6dcd7..08ff1e11 100644 --- a/cmd/internal/app_store_connect/main.go +++ b/cmd/internal/app_store_connect/main.go @@ -54,7 +54,7 @@ const ( groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda" ) -func createClient() *asc.Client { +func createClient() *Client { privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH")) if err != nil { log.Fatal(err) @@ -63,7 +63,7 @@ func createClient() *asc.Client { if err != nil { log.Fatal(err) } - return asc.NewClient(tokenConfig.Client()) + return &Client{asc.NewClient(tokenConfig.Client())} } func fetchMacOSVersion(ctx context.Context) error { @@ -242,10 +242,14 @@ func prepareAppStore(ctx context.Context) error { log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) } log.Info(string(platform), " ", tag, " update build") - _, _, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID) + response, err = client.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID) if err != nil { return err } + if response.StatusCode != http.StatusNoContent { + response.Write(os.Stderr) + log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status) + } } else { switch *version.Attributes.AppStoreState { case asc.AppStoreVersionStatePrepareForSubmission,