diff --git a/cmd/berty-release/build.go b/cmd/berty-release/build.go index 964e58ad..9c2d4420 100644 --- a/cmd/berty-release/build.go +++ b/cmd/berty-release/build.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "strconv" "github.com/spf13/cobra" ) @@ -19,12 +18,12 @@ var buildListCmd = &cobra.Command{ Short: "list last 30 builds, if branch is omitted list all 30 recent builds", Run: func(cmd *cobra.Command, args []string) { var pull string - if len(args) > 0 && args[0] != "" { + if len(args) > 0 { pull = args[0] } - bs, err := cfg.client.Builds(pull, 30, 0) + bs, err := cfg.client.Builds(pull, "", 30, 0) if err != nil { fmt.Println("client error: ", err) return @@ -42,13 +41,7 @@ var buildGetCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Short: "get build info", Run: func(cmd *cobra.Command, args []string) { - bn, err := strconv.Atoi(args[0]) - if err != nil { - fmt.Println("args error: ", err) - return - } - - bs, err := cfg.client.Build(bn) + bs, err := cfg.client.Build(args[0]) if err != nil { fmt.Println("client error: ", err) return @@ -66,13 +59,7 @@ var buildGetArtifactsCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Short: "get build artifacts", Run: func(cmd *cobra.Command, args []string) { - bn, err := strconv.Atoi(args[0]) - if err != nil { - fmt.Println("args error: ", err) - return - } - - arts, err := cfg.client.GetArtifacts(bn, true) + arts, err := cfg.client.GetArtifacts(args[0], true) if err != nil { fmt.Println("client error: ", err) return diff --git a/cmd/berty-release/server.go b/cmd/berty-release/server.go index 6f96516c..b0c945d8 100644 --- a/cmd/berty-release/server.go +++ b/cmd/berty-release/server.go @@ -6,7 +6,8 @@ import ( ) type serverConfig struct { - addr string + addr string + hostname string } var serverCfg serverConfig @@ -15,13 +16,14 @@ var serveCmd = &cobra.Command{ Use: "serve", Short: "Server release tool", Run: func(cmd *cobra.Command, args []string) { - s := server.NewServer(cfg.client) + s := server.NewServer(cfg.client, serverCfg.hostname) panic(s.Start(serverCfg.addr)) }, } func init() { serveCmd.PersistentFlags().StringVarP(&serverCfg.addr, "listen", "l", ":3670", "Listen addr") + serveCmd.PersistentFlags().StringVarP(&serverCfg.hostname, "hostname", "n", "localhost", "hostname") rootCmd.AddCommand(serveCmd) } diff --git a/pkg/circle/build.go b/pkg/circle/build.go index ebfba928..788c801f 100644 --- a/pkg/circle/build.go +++ b/pkg/circle/build.go @@ -1,19 +1,64 @@ package circle import ( + "bytes" + "io" + "strconv" + circleci "github.com/jszwedko/go-circleci" ) -func (c *Client) Builds(pull string, limit, offset int) ([]*circleci.Build, error) { - return c.ci.ListRecentBuildsForProject(c.username, c.repo, pull, "", limit, offset) +func (c *Client) Builds(pull string, job string, limit, offset int) ([]*circleci.Build, error) { + bs, err := c.ci.ListRecentBuildsForProject(c.username, c.repo, pull, "", limit, offset) + if job == "" { + return bs, err + } + + var jbuild []*circleci.Build + for _, b := range bs { + if j, ok := b.BuildParameters["CIRCLE_JOB"]; ok { + if j == job { + jbuild = append(jbuild, b) + } + + } + } + + return jbuild, nil +} + +func (c *Client) Build(build string) (*circleci.Build, error) { + i, err := strconv.Atoi(build) + if err != nil { + return nil, err + } + + return c.ci.GetBuild(c.username, c.repo, i) } -func (c *Client) Build(nbuild int) (*circleci.Build, error) { - return c.ci.GetBuild(c.username, c.repo, nbuild) +func (c *Client) GetArtifact(art *circleci.Artifact) (io.ReadCloser, error) { + res, err := c.http.Get(art.URL) + return res.Body, err } -func (c *Client) GetArtifacts(nbuild int, token bool) ([]*circleci.Artifact, error) { - arts, err := c.ci.ListBuildArtifacts(c.username, c.repo, nbuild) +func (c *Client) GetRawArtifact(art *circleci.Artifact) ([]byte, int64, error) { + body, err := c.GetArtifact(art) + + buf := &bytes.Buffer{} + nRead, err := io.Copy(buf, body) + + body.Close() + + return buf.Bytes(), nRead, err +} + +func (c *Client) GetArtifacts(build string, token bool) ([]*circleci.Artifact, error) { + i, err := strconv.Atoi(build) + if err != nil { + return nil, err + } + + arts, err := c.ci.ListBuildArtifacts(c.username, c.repo, i) if err != nil { return nil, err } diff --git a/pkg/circle/client.go b/pkg/circle/client.go index e4b9200c..38906259 100644 --- a/pkg/circle/client.go +++ b/pkg/circle/client.go @@ -14,6 +14,8 @@ type Client struct { username string repo string token string + + http *http.Client } func New(token, username, repo string) *Client { @@ -22,42 +24,11 @@ func New(token, username, repo string) *Client { } ci := &circleci.Client{Token: token, HTTPClient: httpclient} - return &Client{ci, username, repo, token} + return &Client{ + ci: ci, + username: username, + repo: repo, + token: token, + http: httpclient, + } } - -// func (c *Client) client() *http.Client { -// return http.DefaultClient -// } - -// func (c *Client) request(bodyStruct interface{}) error { -// req, err := http.NewRequest(method, u.String(), nil) -// if err != nil { -// return err -// } - -// req.Header.Add("Accept", "application/json") -// req.Header.Add("Content-Type", "application/json") - -// if bodyStruct != nil { -// b, err := json.Marshal(bodyStruct) -// if err != nil { -// return err -// } - -// req.Body = nopCloser{bytes.NewBuffer(b)} -// } - -// resp, err := c.client().Do(req) -// if err != nil { -// return err -// } -// defer resp.Body.Close() - -// if responseStruct != nil { -// err = json.NewDecoder(resp.Body).Decode(responseStruct) -// if err != nil { -// return err -// } -// } - -// } diff --git a/server/asset.go b/server/asset.go new file mode 100644 index 00000000..499de8e4 --- /dev/null +++ b/server/asset.go @@ -0,0 +1,58 @@ +package server + +import "howett.net/plist" + +var ApplePlistHeader = ` + +` + +type ApplePlistAsset struct { + Kind string `plist:"kind"` + URL string `plist:"url"` // kind, url +} + +type ApplePlistMetadata struct { + BundleIdentifier string `plist:"bundle-identifier"` + BundleVersion string `plist:"bundle-version"` + Kind string `plist:"kind"` + Title string `plist:"title"` +} + +type ApplePlistItem struct { + Assets []*ApplePlistAsset `plist:"assets"` + Metadata *ApplePlistMetadata `plist:"metadata"` +} + +type ApplePlistRelease struct { + Items []*ApplePlistItem +} + +func NewPlistRelease(bundle, version, title, url string) ([]byte, error) { + meta := &ApplePlistMetadata{ + BundleIdentifier: bundle, + BundleVersion: version, + Kind: "software", + Title: title, + } + + assets := []*ApplePlistAsset{ + &ApplePlistAsset{ + Kind: "software-package", + URL: url, + }, + } + + items := []*ApplePlistItem{ + &ApplePlistItem{ + Assets: assets, + Metadata: meta, + }, + } + + release := &ApplePlistRelease{ + Items: items, + } + + return plist.MarshalIndent(release, plist.XMLFormat, "\t") +} diff --git a/server/handler.go b/server/handler.go index 693168f0..54773f0c 100644 --- a/server/handler.go +++ b/server/handler.go @@ -1,23 +1,133 @@ package server import ( + "fmt" "net/http" + "regexp" "strconv" + "strings" + circleci "github.com/jszwedko/go-circleci" "github.com/labstack/echo" ) +const ( + BUNDLE_ID = "chat.berty.ios" + APP_NAME = "berty" + JOB_IOS = "client.rn.ios" +) + +var reIPA = regexp.MustCompile("/([^/]+).ipa$") +var reVersion = regexp.MustCompile("/version$") + func (s *Server) Build(c echo.Context) error { id := c.Param("build_id") - i, err := strconv.Atoi(id) + ret, err := s.client.Build(id) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.JSON(http.StatusOK, ret) +} + +func (s *Server) Builds(c echo.Context) error { + pull := c.Param("branch") + ret, err := s.client.Builds(pull, "", 30, 0) if err != nil { - return c.JSON(http.StatusInternalServerError, err) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - ret, err := s.client.Build(i) + return c.JSON(http.StatusOK, ret) +} + +func (s *Server) Artifacts(c echo.Context) error { + id := c.Param("build_id") + ret, err := s.client.GetArtifacts(id, true) if err != nil { - return c.JSON(http.StatusInternalServerError, err) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, ret) } + +func (s *Server) getVersion(arts []*circleci.Artifact, kind string) (string, error) { + for _, art := range arts { + if !reVersion.MatchString(art.PrettyPath) { + continue + } + + ret, n, err := s.client.GetRawArtifact(art) + if err != nil { + return "", err + } + + s := string(ret[:n]) + for _, l := range strings.Split(s, "\n") { + if strings.HasPrefix(l, kind) { + s := strings.Split(l, ":") + if len(s) == 2 { + return s[1], nil + } + } + } + + return "", fmt.Errorf("found malformated version") + } + + return "", fmt.Errorf("no version found") +} + +func (s *Server) GetIPA(c echo.Context) error { + id := c.Param("build_id") + arts, err := s.client.GetArtifacts(id, true) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + for _, art := range arts { + if !reIPA.MatchString(art.PrettyPath) { + continue + } + + rc, err := s.client.GetArtifact(art) + if err != nil { + return err + } + + return c.Stream(http.StatusOK, "application/octet-stream", rc) + } + + return echo.NewHTTPError(http.StatusInternalServerError, "IPA not found") +} + +func (s *Server) ReleaseIOS(c echo.Context) error { + pull := c.Param("*") + builds, err := s.client.Builds(pull, JOB_IOS, 30, 0) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + if len(builds) == 0 { + return echo.NewHTTPError(http.StatusInternalServerError, "no valid build(s) found") + } + + id := strconv.Itoa(builds[0].BuildNum) + arts, err := s.client.GetArtifacts(id, true) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + version, err := s.getVersion(arts, "ios") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + url := fmt.Sprintf("itms-services://?action=download-manifest&url=http://%s/ipa/build/%s", s.hostname, id) + + plist, err := NewPlistRelease(BUNDLE_ID, version, APP_NAME, url) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.Blob(http.StatusOK, "application/x-plist", plist) +} diff --git a/server/server.go b/server/server.go index 7fa4ccbe..6f667e79 100644 --- a/server/server.go +++ b/server/server.go @@ -10,18 +10,20 @@ type httperror struct { } type Server struct { - client *circle.Client - e *echo.Echo + client *circle.Client + hostname string + e *echo.Echo } -func NewServer(client *circle.Client) *Server { +func NewServer(client *circle.Client, hostname string) *Server { e := echo.New() - s := &Server{client, e} + s := &Server{client, hostname, e} e.GET("/build/:build_id", s.Build) - // e.GET("/builds", e.Build) - // e.GET("/artifacts/:build_id", e.Build) - // e.GET("/release/:build_id", e.Build) + e.GET("/builds/*", s.Builds) + e.GET("/ipa/build/:build_id", s.GetIPA) + e.GET("/release/ios/*", s.ReleaseIOS) + e.GET("/artifacts/:build_id", s.Artifacts) return s }