diff --git a/dewy.go b/dewy.go index d0b4b96..0dbd1b0 100644 --- a/dewy.go +++ b/dewy.go @@ -17,7 +17,9 @@ import ( starter "github.com/lestrrat-go/server-starter" "github.com/linyows/dewy/kvs" "github.com/linyows/dewy/notice" + "github.com/linyows/dewy/registory" "github.com/linyows/dewy/repo" + "github.com/linyows/dewy/storage" ) const ( @@ -30,7 +32,8 @@ const ( // Dewy struct type Dewy struct { config Config - repo repo.Repo + registory registory.Registory + fetcher storage.Fetcher cache kvs.KVS isServerRunning bool root string @@ -57,7 +60,8 @@ func New(c Config) (*Dewy, error) { return &Dewy{ config: c, cache: kv, - repo: r, + registory: r, + fetcher: r, isServerRunning: false, root: wd, }, nil @@ -69,15 +73,20 @@ func (d *Dewy) Start(i int) { defer cancel() var err error - d.notice, err = notice.New(¬ice.Slack{Meta: ¬ice.Config{ - RepoOwnerLink: d.repo.OwnerURL(), - RepoOwnerIcon: d.repo.OwnerIconURL(), - RepoLink: d.repo.URL(), - RepoOwner: d.config.Repository.Owner, - RepoName: d.config.Repository.Name, - Source: d.config.Repository.Artifact, - Command: d.config.Command.String(), - }}) + nc := ¬ice.Config{ + RepoOwner: d.config.Repository.Owner, + RepoName: d.config.Repository.Name, + Source: d.config.Repository.Artifact, + Command: d.config.Command.String(), + } + repo, ok := d.registory.(repo.Repo) + if ok { + nc.RepoOwnerLink = repo.OwnerURL() + nc.RepoOwnerIcon = repo.OwnerIconURL() + nc.RepoLink = repo.URL() + } + + d.notice, err = notice.New(¬ice.Slack{Meta: nc}) if err != nil { log.Printf("[ERROR] Notice failure: %#v", err) return @@ -114,7 +123,7 @@ func (d *Dewy) Run() error { defer cancel() // Get current - res, err := d.repo.Current(&repo.CurrentRequest{ + res, err := d.registory.Current(®istory.CurrentRequest{ ArtifactName: d.config.Repository.Artifact, }) if err != nil { @@ -148,7 +157,7 @@ func (d *Dewy) Run() error { // Download artifact and cache if !found { buf := new(bytes.Buffer) - if err := d.repo.Download(buf); err != nil { + if err := d.fetcher.Fetch(res.ArtifactURL, buf); err != nil { return err } if err := d.cache.Write(cacheKey, buf.Bytes()); err != nil { @@ -159,7 +168,7 @@ func (d *Dewy) Run() error { if d.notice != nil { d.notice.Notify(ctx, fmt.Sprintf("New shipping <%s|%s> was detected", - d.repo.ReleaseURL(), d.repo.ReleaseTag())) + res.ArtifactURL, res.Tag)) } if err := d.deploy(cacheKey); err != nil { @@ -180,7 +189,10 @@ func (d *Dewy) Run() error { } log.Print("[DEBUG] Record shipping") - err = d.repo.RecordShipping() + err = d.registory.Report(®istory.ReportRequest{ + ID: res.ID, + Tag: res.Tag, + }) if err != nil { log.Printf("[ERROR] Record shipping failure: %#v", err) } diff --git a/dewy_test.go b/dewy_test.go index 089393f..5231299 100644 --- a/dewy_test.go +++ b/dewy_test.go @@ -31,7 +31,8 @@ func TestNew(t *testing.T) { } expect := &Dewy{ config: c, - repo: r, + registory: r, + fetcher: r, cache: dewy.cache, isServerRunning: false, root: wd, diff --git a/registory/registory.go b/registory/registory.go new file mode 100644 index 0000000..3e4a2e4 --- /dev/null +++ b/registory/registory.go @@ -0,0 +1,36 @@ +package registory + +type Registory interface { + // Current returns the current artifact. + Current(*CurrentRequest) (*CurrentResponse, error) + // Report reports the result of deploying the artifact. + Report(*ReportRequest) error +} + +// CurrentRequest is the request to get the current artifact. +type CurrentRequest struct { + // ArtifactName is the name of the artifact to fetch. + // FIXME: If possible, ArtifactName should be optional. + ArtifactName string +} + +// CurrentResponse is the response to get the current artifact. +type CurrentResponse struct { + // ID uniquely identifies the response. + ID string + // Tag uniquely identifies the artifact concerned. + Tag string + // ArtifactURL is the URL to download the artifact. + // The URL is not only "https://" + ArtifactURL string +} + +// ReportRequest is the request to report the result of deploying the artifact. +type ReportRequest struct { + // ID is the ID of the response. + ID string + // Tag is the current tag of deployed artifact. + Tag string + // Err is the error that occurred during deployment. If Err is nil, the deployment is considered successful. + Err error +} diff --git a/repo/github_release.go b/repo/github_release.go index f06d3ce..83c5610 100644 --- a/repo/github_release.go +++ b/repo/github_release.go @@ -15,9 +15,11 @@ import ( "github.com/google/go-github/v55/github" "github.com/google/go-querystring/query" "github.com/k1LoW/go-github-client/v55/factory" + "github.com/linyows/dewy/registory" ) const ( + GitHubReleaseScheme = "github_release" // ISO8601 for time format ISO8601 = "20060102T150405Z0700" ) @@ -26,18 +28,15 @@ var httpClient = &http.Client{ Timeout: 30 * time.Second, } +var _ Repo = (*GithubRelease)(nil) + // GithubRelease struct type GithubRelease struct { baseURL string uploadURL string owner string name string - artifact string downloadURL string - releaseID int64 - assetID int64 - releaseURL string - releaseTag string prerelease bool disableRecordShipping bool // FIXME: For testing. Remove this. cl *github.Client @@ -53,7 +52,6 @@ func NewGithubRelease(c Config) (*GithubRelease, error) { g := &GithubRelease{ owner: c.Owner, name: c.Name, - artifact: c.Artifact, prerelease: c.PreRelease, disableRecordShipping: c.DisableRecordShipping, cl: cl, @@ -92,33 +90,19 @@ func (g *GithubRelease) URL() string { return fmt.Sprintf("%s/%s", g.OwnerURL(), g.name) } -// ReleaseTag returns tag -func (g *GithubRelease) ReleaseTag() string { - return g.releaseTag -} - // ReleaseURL returns release URL -func (g *GithubRelease) ReleaseURL() string { - return g.releaseURL -} - -func (g *GithubRelease) Current(req *CurrentRequest) (*CurrentResponse, error) { +func (g *GithubRelease) Current(req *registory.CurrentRequest) (*registory.CurrentResponse, error) { release, err := g.latest() if err != nil { return nil, err } - g.releaseID = *release.ID - g.releaseURL = *release.HTMLURL - found := false for _, v := range release.Assets { if v.GetName() == req.ArtifactName { found = true log.Printf("[DEBUG] Fetched: %+v", v) g.downloadURL = v.GetBrowserDownloadURL() - g.releaseTag = release.GetTagName() - g.assetID = v.GetID() g.updatedAt = v.GetUpdatedAt() break } @@ -129,7 +113,8 @@ func (g *GithubRelease) Current(req *CurrentRequest) (*CurrentResponse, error) { au := fmt.Sprintf("github_release://%s/%s/tag/%s/%s", g.owner, g.name, release.GetTagName(), req.ArtifactName) - return &CurrentResponse{ + return ®istory.CurrentResponse{ + ID: time.Now().Format(ISO8601), Tag: release.GetTagName(), ArtifactURL: au, }, nil @@ -158,9 +143,53 @@ func (g *GithubRelease) latest() (*github.RepositoryRelease, error) { return r, nil } -func (g *GithubRelease) Download(w io.Writer) error { +func (g *GithubRelease) Fetch(url string, w io.Writer) error { ctx := context.Background() - reader, url, err := g.cl.Repositories.DownloadReleaseAsset(ctx, g.owner, g.name, g.assetID, httpClient) + // github_release://owner/repo/tag/v1.0.0/artifact.zip + // github_release://owner/repo/latest/artifact.zip + splitted := strings.Split(strings.TrimPrefix(url, fmt.Sprintf("%s://", GitHubReleaseScheme)), "/") + if len(splitted) != 4 && len(splitted) != 5 { + return fmt.Errorf("invalid url: %s", url) + } + owner := splitted[0] + name := splitted[1] + if len(splitted) == 4 { + // latest + // FIXME: not implemented + return fmt.Errorf("not implemented") + } + tag := splitted[3] + artifactName := splitted[4] + page := 1 + var assetID int64 +L: + for { + releases, res, err := g.cl.Repositories.ListReleases(ctx, g.owner, g.name, &github.ListOptions{ + Page: page, + PerPage: 100, + }) + if err != nil { + return err + } + for _, r := range releases { + if r.GetTagName() != tag { + continue + } + for _, a := range r.Assets { + if a.GetName() != artifactName { + continue + } + assetID = a.GetID() + break L + } + } + if res.NextPage == 0 { + break + } + page = res.NextPage + } + + reader, url, err := g.cl.Repositories.DownloadReleaseAsset(ctx, owner, name, assetID, httpClient) if err != nil { return err } @@ -181,38 +210,60 @@ func (g *GithubRelease) Download(w io.Writer) error { return nil } -// RecordShipping save shipping to github -func (g *GithubRelease) RecordShipping() error { +func (g *GithubRelease) Report(req *registory.ReportRequest) error { if g.disableRecordShipping { return nil } + if req.Err != nil { + return req.Err + } ctx := context.Background() now := time.Now().UTC().Format(ISO8601) hostname, _ := os.Hostname() info := fmt.Sprintf("shipped to %s at %s", strings.ToLower(hostname), now) - s := fmt.Sprintf("repos/%s/%s/releases/%d/assets", g.owner, g.name, g.releaseID) - opt := &github.UploadOptions{Name: strings.Replace(info, " ", "_", -1) + ".txt"} + page := 1 + for { + releases, res, err := g.cl.Repositories.ListReleases(ctx, g.owner, g.name, &github.ListOptions{ + Page: page, + PerPage: 100, + }) + if err != nil { + return err + } + for _, r := range releases { + if r.GetTagName() == req.Tag { + s := fmt.Sprintf("repos/%s/%s/releases/%d/assets", g.owner, g.name, r.GetID()) + opt := &github.UploadOptions{Name: strings.Replace(info, " ", "_", -1) + ".txt"} - u, err := url.Parse(s) - if err != nil { - return err - } - qs, err := query.Values(opt) - if err != nil { - return err - } - u.RawQuery = qs.Encode() + u, err := url.Parse(s) + if err != nil { + return err + } + qs, err := query.Values(opt) + if err != nil { + return err + } + u.RawQuery = qs.Encode() + b := []byte(info) + r := bytes.NewReader(b) + req, err := g.cl.NewUploadRequest(u.String(), r, int64(len(b)), "text/plain") + if err != nil { + return err + } - byteData := []byte(info) - r := bytes.NewReader(byteData) - req, err := g.cl.NewUploadRequest(u.String(), r, int64(len(byteData)), "text/plain") - if err != nil { - return err + asset := new(github.ReleaseAsset) + if _, err := g.cl.Do(ctx, req, asset); err != nil { + return err + } + return nil + } + } + if res.NextPage == 0 { + break + } + page = res.NextPage } - asset := new(github.ReleaseAsset) - _, err = g.cl.Do(ctx, req, asset) - - return err + return fmt.Errorf("release not found: %s", req.Tag) } diff --git a/repo/repo.go b/repo/repo.go index e7ae057..448d85b 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -2,36 +2,19 @@ package repo import ( "errors" - "io" "path" -) - -// CurrentRequest is the request to get the current artifact. -type CurrentRequest struct { - // ArtifactName is the name of the artifact to fetch. - // FIXME: If possible, ArtifactName should be optional. - ArtifactName string -} -// CurrentResponse is the response to get the current artifact. -type CurrentResponse struct { - // Tag uniquely identifies the artifact concerned. - Tag string - // ArtifactURL is the URL to download the artifact. - // The URL is not only "https://" - ArtifactURL string -} + "github.com/linyows/dewy/registory" + "github.com/linyows/dewy/storage" +) // Repo interface for repository type Repo interface { + registory.Registory + storage.Fetcher String() string - RecordShipping() error - ReleaseTag() string - ReleaseURL() string OwnerURL() string OwnerIconURL() string - Current(req *CurrentRequest) (*CurrentResponse, error) - Download(w io.Writer) error URL() string } diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..277c13b --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,9 @@ +package storage + +import "io" + +// Fetcher is the interface that wraps the Fetch method. +type Fetcher interface { + // Fetch fetches the artifact from the storage. + Fetch(url string, w io.Writer) error +}