diff --git a/README.md b/README.md index eae9860..610ad5d 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,23 @@ Fetches and creates versioned GitHub resources. ## Behavior -### `check`: Extract versions from the bucket. +### `check`: Check for released versions. -*TODO* +Releases are listed and sorted by their tag, using +[semver](http://semver.org) semantics if possible. -### `in`: Fetch an object from the bucket. +### `in`: Fetch assets from a release. -*TODO* +Fetches artifacts from the given release version. If the version is not +specified, the latest version is chosen using [semver](http://semver.org) +semantics. -### `out`: Upload an object to the bucket. +#### Parameters + +* `globs`: *Optional.* A list of globs for files that will be downloaded from + the release. If not specified, all assets will be fetched. + +### `out`: Publish a release. Given a name specified in `name`, a body specified in `body`, and the tag to use specified in `tag`, this creates a release on GitHub then uploads the files diff --git a/check_command.go b/check_command.go index 47f1687..f5e8f4a 100644 --- a/check_command.go +++ b/check_command.go @@ -72,12 +72,12 @@ func (r byVersion) Less(i, j int) bool { return false } - first, err := semver.New(*r[i].TagName) + first, err := semver.New(dropLeadingAlpha(*r[i].TagName)) if err != nil { return true } - second, err := semver.New(*r[j].TagName) + second, err := semver.New(dropLeadingAlpha(*r[j].TagName)) if err != nil { return false } diff --git a/check_command_test.go b/check_command_test.go index 817165c..76c6969 100644 --- a/check_command_test.go +++ b/check_command_test.go @@ -44,9 +44,9 @@ var _ = Describe("Check Command", func() { Context("when there are releases", func() { BeforeEach(func() { returnedReleases = []github.RepositoryRelease{ - {TagName: github.String("0.4.0")}, + {TagName: github.String("v0.4.0")}, {TagName: github.String("0.1.3")}, - {TagName: github.String("0.1.2")}, + {TagName: github.String("v0.1.2")}, } }) @@ -58,7 +58,7 @@ var _ = Describe("Check Command", func() { Ω(response).Should(HaveLen(1)) Ω(response[0]).Should(Equal(resource.Version{ - Tag: "0.4.0", + Tag: "v0.4.0", })) }) }) @@ -79,9 +79,9 @@ var _ = Describe("Check Command", func() { Context("when there are releases", func() { BeforeEach(func() { returnedReleases = []github.RepositoryRelease{ - {TagName: github.String("0.1.4")}, + {TagName: github.String("v0.1.4")}, {TagName: github.String("0.4.0")}, - {TagName: github.String("0.1.3")}, + {TagName: github.String("v0.1.3")}, {TagName: github.String("0.1.2")}, } }) @@ -104,13 +104,13 @@ var _ = Describe("Check Command", func() { response, err := command.Run(resource.CheckRequest{ Version: resource.Version{ - Tag: "0.1.3", + Tag: "v0.1.3", }, }) Ω(err).ShouldNot(HaveOccurred()) Ω(response).Should(Equal([]resource.Version{ - {Tag: "0.1.4"}, + {Tag: "v0.1.4"}, {Tag: "0.4.0"}, })) }) diff --git a/cmd/in/in.go b/cmd/in/in.go new file mode 100644 index 0000000..6d7245b --- /dev/null +++ b/cmd/in/in.go @@ -0,0 +1,41 @@ +package main + +import ( + "encoding/json" + "os" + + "github.com/concourse/github-release-resource" +) + +func main() { + if len(os.Args) < 2 { + resource.Sayf("usage: %s \n", os.Args[0]) + os.Exit(1) + } + + var request resource.InRequest + inputRequest(&request) + + destDir := os.Args[1] + + github := resource.NewGitHubClient(request.Source) + command := resource.NewInCommand(github) + response, err := command.Run(destDir, request) + if err != nil { + resource.Fatal("running command", err) + } + + outputResponse(response) +} + +func inputRequest(request *resource.InRequest) { + if err := json.NewDecoder(os.Stdin).Decode(request); err != nil { + resource.Fatal("reading request from stdin", err) + } +} + +func outputResponse(response resource.InResponse) { + if err := json.NewEncoder(os.Stdout).Encode(response); err != nil { + resource.Fatal("writing response to stdout", err) + } +} diff --git a/fakes/fake_git_hub.go b/fakes/fake_git_hub.go index 67dce0d..aa2fb14 100644 --- a/fakes/fake_git_hub.go +++ b/fakes/fake_git_hub.go @@ -26,6 +26,15 @@ type FakeGitHub struct { result1 *github.RepositoryRelease result2 error } + ListReleaseAssetsStub func(release *github.RepositoryRelease) ([]github.ReleaseAsset, error) + listReleaseAssetsMutex sync.RWMutex + listReleaseAssetsArgsForCall []struct { + release *github.RepositoryRelease + } + listReleaseAssetsReturns struct { + result1 []github.ReleaseAsset + result2 error + } UploadReleaseAssetStub func(release *github.RepositoryRelease, name string, file *os.File) error uploadReleaseAssetMutex sync.RWMutex uploadReleaseAssetArgsForCall []struct { @@ -96,6 +105,39 @@ func (fake *FakeGitHub) CreateReleaseReturns(result1 *github.RepositoryRelease, }{result1, result2} } +func (fake *FakeGitHub) ListReleaseAssets(release *github.RepositoryRelease) ([]github.ReleaseAsset, error) { + fake.listReleaseAssetsMutex.Lock() + fake.listReleaseAssetsArgsForCall = append(fake.listReleaseAssetsArgsForCall, struct { + release *github.RepositoryRelease + }{release}) + fake.listReleaseAssetsMutex.Unlock() + if fake.ListReleaseAssetsStub != nil { + return fake.ListReleaseAssetsStub(release) + } else { + return fake.listReleaseAssetsReturns.result1, fake.listReleaseAssetsReturns.result2 + } +} + +func (fake *FakeGitHub) ListReleaseAssetsCallCount() int { + fake.listReleaseAssetsMutex.RLock() + defer fake.listReleaseAssetsMutex.RUnlock() + return len(fake.listReleaseAssetsArgsForCall) +} + +func (fake *FakeGitHub) ListReleaseAssetsArgsForCall(i int) *github.RepositoryRelease { + fake.listReleaseAssetsMutex.RLock() + defer fake.listReleaseAssetsMutex.RUnlock() + return fake.listReleaseAssetsArgsForCall[i].release +} + +func (fake *FakeGitHub) ListReleaseAssetsReturns(result1 []github.ReleaseAsset, result2 error) { + fake.ListReleaseAssetsStub = nil + fake.listReleaseAssetsReturns = struct { + result1 []github.ReleaseAsset + result2 error + }{result1, result2} +} + func (fake *FakeGitHub) UploadReleaseAsset(release *github.RepositoryRelease, name string, file *os.File) error { fake.uploadReleaseAssetMutex.Lock() fake.uploadReleaseAssetArgsForCall = append(fake.uploadReleaseAssetArgsForCall, struct { diff --git a/github.go b/github.go index d227c2b..90dddd5 100644 --- a/github.go +++ b/github.go @@ -13,6 +13,8 @@ import ( type GitHub interface { ListReleases() ([]github.RepositoryRelease, error) CreateRelease(release *github.RepositoryRelease) (*github.RepositoryRelease, error) + + ListReleaseAssets(release *github.RepositoryRelease) ([]github.ReleaseAsset, error) UploadReleaseAsset(release *github.RepositoryRelease, name string, file *os.File) error } @@ -57,6 +59,15 @@ func (g *GitHubClient) CreateRelease(release *github.RepositoryRelease) (*github return createdRelease, nil } +func (g *GitHubClient) ListReleaseAssets(release *github.RepositoryRelease) ([]github.ReleaseAsset, error) { + assets, _, err := g.client.Repositories.ListReleaseAssets(g.user, g.repository, *release.ID, nil) + if err != nil { + return []github.ReleaseAsset{}, nil + } + + return assets, nil +} + func (g *GitHubClient) UploadReleaseAsset(release *github.RepositoryRelease, name string, file *os.File) error { _, _, err := g.client.Repositories.UploadReleaseAsset( g.user, diff --git a/in_command.go b/in_command.go new file mode 100644 index 0000000..bdba78a --- /dev/null +++ b/in_command.go @@ -0,0 +1,116 @@ +package resource + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "sort" + + "github.com/google/go-github/github" +) + +type InCommand struct { + github GitHub +} + +func NewInCommand(github GitHub) *InCommand { + return &InCommand{ + github: github, + } +} + +func (c *InCommand) Run(destDir string, request InRequest) (InResponse, error) { + releases, err := c.github.ListReleases() + if err != nil { + return InResponse{}, nil + } + + sort.Sort(byVersion(releases)) + + if len(releases) == 0 { + return InResponse{}, errors.New("no releases") + } + + var foundRelease *github.RepositoryRelease + + if request.Version == nil { + foundRelease = &releases[len(releases)-1] + } else { + for _, release := range releases { + if *release.TagName == request.Version.Tag { + foundRelease = &release + break + } + } + } + + if foundRelease == nil { + return InResponse{}, fmt.Errorf("could not find release with tag: %s", request.Version.Tag) + } + + assets, err := c.github.ListReleaseAssets(foundRelease) + if err != nil { + return InResponse{}, nil + } + + for _, asset := range assets { + url := *asset.BrowserDownloadURL + path := filepath.Join(destDir, *asset.Name) + + var matchFound bool + if len(request.Params.Globs) == 0 { + matchFound = true + } else { + for _, glob := range request.Params.Globs { + matches, err := filepath.Match(glob, *asset.Name) + if err != nil { + return InResponse{}, err + } + + if matches { + matchFound = true + break + } + } + } + + if !matchFound { + continue + } + + err := c.downloadFile(url, path) + if err != nil { + return InResponse{}, nil + } + } + + return InResponse{ + Version: Version{ + Tag: *foundRelease.TagName, + }, + }, nil +} + +func (c *InCommand) downloadFile(url, destPath string) error { + out, err := os.Create(destPath) + if err != nil { + return err + } + defer out.Close() + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + return nil +} diff --git a/in_command_test.go b/in_command_test.go new file mode 100644 index 0000000..b984a3d --- /dev/null +++ b/in_command_test.go @@ -0,0 +1,206 @@ +package resource_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/google/go-github/github" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/ghttp" + + "github.com/concourse/github-release-resource" + "github.com/concourse/github-release-resource/fakes" +) + +var _ = Describe("In Command", func() { + var ( + command *resource.InCommand + githubClient *fakes.FakeGitHub + server *ghttp.Server + + inRequest resource.InRequest + + inResponse resource.InResponse + inErr error + + destDir string + ) + + BeforeEach(func() { + var err error + + githubClient = &fakes.FakeGitHub{} + command = resource.NewInCommand(githubClient) + + destDir, err = ioutil.TempDir("", "github-release") + Ω(err).ShouldNot(HaveOccurred()) + + server = ghttp.NewServer() + server.RouteToHandler("GET", "/example.txt", ghttp.RespondWith(200, "example.txt")) + server.RouteToHandler("GET", "/example.rtf", ghttp.RespondWith(200, "example.rtf")) + server.RouteToHandler("GET", "/example.wtf", ghttp.RespondWith(200, "example.wtf")) + + inRequest = resource.InRequest{} + }) + + JustBeforeEach(func() { + inResponse, inErr = command.Run(destDir, inRequest) + }) + + AfterEach(func() { + server.Close() + Ω(os.RemoveAll(destDir)).Should(Succeed()) + }) + + buildRelease := func(id int, tag string) github.RepositoryRelease { + return github.RepositoryRelease{ + ID: &id, + TagName: &tag, + } + } + + buildAsset := func(name string) github.ReleaseAsset { + url := server.URL() + "/" + name + + return github.ReleaseAsset{ + Name: &name, + BrowserDownloadURL: &url, + } + } + + Context("when there are releases", func() { + BeforeEach(func() { + githubClient.ListReleasesReturns([]github.RepositoryRelease{ + buildRelease(2, "v0.35.0"), + buildRelease(1, "v0.34.0"), + }, nil) + + githubClient.ListReleaseAssetsReturns([]github.ReleaseAsset{ + buildAsset("example.txt"), + buildAsset("example.rtf"), + buildAsset("example.wtf"), + }, nil) + }) + + Context("when a present version is specified", func() { + BeforeEach(func() { + inRequest.Version = &resource.Version{ + Tag: "v0.35.0", + } + }) + + Context("when valid asset filename globs are given", func() { + BeforeEach(func() { + inRequest.Params = resource.InParams{ + Globs: []string{"*.txt", "*.rtf"}, + } + }) + + It("succeeds", func() { + Ω(inErr).ShouldNot(HaveOccurred()) + }) + + It("returns the fetched version", func() { + Ω(inResponse.Version).Should(Equal(resource.Version{Tag: "v0.35.0"})) + }) + + It("downloads only the files that match the globs", func() { + _, err := os.Stat(filepath.Join(destDir, "example.txt")) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "example.rtf")) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "example.wtf")) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when an invalid asset filename glob is given", func() { + BeforeEach(func() { + inRequest.Params = resource.InParams{ + Globs: []string{`[`}, + } + }) + + It("returns an error", func() { + Ω(inErr).Should(HaveOccurred()) + }) + }) + + Context("when no globs are specified", func() { + BeforeEach(func() { + inRequest.Source = resource.Source{} + }) + + It("succeeds", func() { + Ω(inErr).ShouldNot(HaveOccurred()) + }) + + It("returns the fetched version", func() { + Ω(inResponse.Version).Should(Equal(resource.Version{Tag: "v0.35.0"})) + }) + + It("downloads all of the files", func() { + _, err := os.Stat(filepath.Join(destDir, "example.txt")) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "example.rtf")) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "example.wtf")) + Ω(err).ShouldNot(HaveOccurred()) + }) + }) + }) + + Context("when the specified version is not available", func() { + BeforeEach(func() { + inRequest.Version = &resource.Version{ + Tag: "v0.36.0", + } + }) + + It("returns an error", func() { + Ω(inErr).Should(HaveOccurred()) + }) + }) + + Context("when the version is not specified", func() { + BeforeEach(func() { + inRequest.Version = nil + }) + + It("succeeds", func() { + Ω(inErr).ShouldNot(HaveOccurred()) + }) + + It("returns the fetched version", func() { + Ω(inResponse.Version).Should(Equal(resource.Version{Tag: "v0.35.0"})) + }) + + It("fetches from the latest release", func() { + _, err := os.Stat(filepath.Join(destDir, "example.txt")) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "example.rtf")) + Ω(err).ShouldNot(HaveOccurred()) + + _, err = os.Stat(filepath.Join(destDir, "example.wtf")) + Ω(err).ShouldNot(HaveOccurred()) + }) + }) + }) + + Context("when no releases are present", func() { + BeforeEach(func() { + githubClient.ListReleasesReturns([]github.RepositoryRelease{}, nil) + }) + + It("returns an error", func() { + Ω(inErr).Should(HaveOccurred()) + }) + }) +}) diff --git a/resources.go b/resources.go index f2da72f..3af0b8a 100644 --- a/resources.go +++ b/resources.go @@ -12,6 +12,21 @@ type CheckRequest struct { Version Version `json:"version"` } +type InRequest struct { + Source Source `json:"source"` + Version *Version `json:"version"` + Params InParams `json:"params"` +} + +type InParams struct { + Globs []string `json:"globs"` +} + +type InResponse struct { + Version Version `json:"version"` + Metadata []MetadataPair `json:"metadata"` +} + type OutRequest struct { Source Source `json:"source"` Params OutParams `json:"params"` diff --git a/strings.go b/strings.go new file mode 100644 index 0000000..fe9025f --- /dev/null +++ b/strings.go @@ -0,0 +1,13 @@ +package resource + +import "unicode" + +func dropLeadingAlpha(s string) string { + for i, r := range s { + if !unicode.IsLetter(r) { + return s[i:] + } + } + + return "" +}