implement /in

fetches a given list of globs from the assets.

also be durable to 'v' prefixing version tags

Signed-off-by: Alex Suraci <asuraci@pivotal.io>
This commit is contained in:
Chris Brown
2015-02-17 18:03:42 -08:00
committed by Alex Suraci
parent 7e47c2696e
commit efbfb91683
10 changed files with 466 additions and 14 deletions

View File

@@ -14,15 +14,23 @@ Fetches and creates versioned GitHub resources.
## Behavior ## 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 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 specified in `tag`, this creates a release on GitHub then uploads the files

View File

@@ -72,12 +72,12 @@ func (r byVersion) Less(i, j int) bool {
return false return false
} }
first, err := semver.New(*r[i].TagName) first, err := semver.New(dropLeadingAlpha(*r[i].TagName))
if err != nil { if err != nil {
return true return true
} }
second, err := semver.New(*r[j].TagName) second, err := semver.New(dropLeadingAlpha(*r[j].TagName))
if err != nil { if err != nil {
return false return false
} }

View File

@@ -44,9 +44,9 @@ var _ = Describe("Check Command", func() {
Context("when there are releases", func() { Context("when there are releases", func() {
BeforeEach(func() { BeforeEach(func() {
returnedReleases = []github.RepositoryRelease{ 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.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).Should(HaveLen(1))
Ω(response[0]).Should(Equal(resource.Version{ Ω(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() { Context("when there are releases", func() {
BeforeEach(func() { BeforeEach(func() {
returnedReleases = []github.RepositoryRelease{ 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.4.0")},
{TagName: github.String("0.1.3")}, {TagName: github.String("v0.1.3")},
{TagName: github.String("0.1.2")}, {TagName: github.String("0.1.2")},
} }
}) })
@@ -104,13 +104,13 @@ var _ = Describe("Check Command", func() {
response, err := command.Run(resource.CheckRequest{ response, err := command.Run(resource.CheckRequest{
Version: resource.Version{ Version: resource.Version{
Tag: "0.1.3", Tag: "v0.1.3",
}, },
}) })
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
Ω(response).Should(Equal([]resource.Version{ Ω(response).Should(Equal([]resource.Version{
{Tag: "0.1.4"}, {Tag: "v0.1.4"},
{Tag: "0.4.0"}, {Tag: "0.4.0"},
})) }))
}) })

41
cmd/in/in.go Normal file
View File

@@ -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 <sources directory>\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)
}
}

View File

@@ -26,6 +26,15 @@ type FakeGitHub struct {
result1 *github.RepositoryRelease result1 *github.RepositoryRelease
result2 error 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 UploadReleaseAssetStub func(release *github.RepositoryRelease, name string, file *os.File) error
uploadReleaseAssetMutex sync.RWMutex uploadReleaseAssetMutex sync.RWMutex
uploadReleaseAssetArgsForCall []struct { uploadReleaseAssetArgsForCall []struct {
@@ -96,6 +105,39 @@ func (fake *FakeGitHub) CreateReleaseReturns(result1 *github.RepositoryRelease,
}{result1, result2} }{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 { func (fake *FakeGitHub) UploadReleaseAsset(release *github.RepositoryRelease, name string, file *os.File) error {
fake.uploadReleaseAssetMutex.Lock() fake.uploadReleaseAssetMutex.Lock()
fake.uploadReleaseAssetArgsForCall = append(fake.uploadReleaseAssetArgsForCall, struct { fake.uploadReleaseAssetArgsForCall = append(fake.uploadReleaseAssetArgsForCall, struct {

View File

@@ -13,6 +13,8 @@ import (
type GitHub interface { type GitHub interface {
ListReleases() ([]github.RepositoryRelease, error) ListReleases() ([]github.RepositoryRelease, error)
CreateRelease(release *github.RepositoryRelease) (*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 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 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 { func (g *GitHubClient) UploadReleaseAsset(release *github.RepositoryRelease, name string, file *os.File) error {
_, _, err := g.client.Repositories.UploadReleaseAsset( _, _, err := g.client.Repositories.UploadReleaseAsset(
g.user, g.user,

116
in_command.go Normal file
View File

@@ -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
}

206
in_command_test.go Normal file
View File

@@ -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())
})
})
})

View File

@@ -12,6 +12,21 @@ type CheckRequest struct {
Version Version `json:"version"` 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 { type OutRequest struct {
Source Source `json:"source"` Source Source `json:"source"`
Params OutParams `json:"params"` Params OutParams `json:"params"`

13
strings.go Normal file
View File

@@ -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 ""
}