diff --git a/Makefile b/Makefile index a1e3476ca..93ed269bf 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ database/testing \ mail \ model \ plugin/deploy \ +plugin/publish \ queue PKGS := $(addprefix github.com/drone/drone/pkg/,$(PKGS)) .PHONY := test $(PKGS) diff --git a/README.md b/README.md index cf6faef1d..ca66f554a 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ publish: container: drone source: /tmp/drone.deb target: latest/drone.deb + branch: master ``` @@ -246,6 +247,11 @@ Drone currently has these `deploy` and `publish` plugins implemented (more to co **publish** - [Amazon s3](#docs) - [OpenStack Swift](#docs) +- [PyPI](#docs) + +Publish plugins can be limited to a specific branch using the `branch` configuration +as seen above in the `swift` example. If you do not specify a `branch` all branches +will be published, with the exception of Pull Requests. ### Notifications diff --git a/pkg/build/build.go b/pkg/build/build.go index d87c82c99..3a4a6ff55 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -517,7 +517,7 @@ func (b *Builder) writeBuildScript(dir string) error { // we should only execute the build commands, // and omit the deploy and publish commands. if len(b.Repo.PR) == 0 { - b.Build.Write(f) + b.Build.Write(f, b.Repo) } else { // only write the build commands b.Build.WriteBuild(f) diff --git a/pkg/build/script/script.go b/pkg/build/script/script.go index d2ca5dfb3..394f0a9bc 100644 --- a/pkg/build/script/script.go +++ b/pkg/build/script/script.go @@ -10,6 +10,7 @@ import ( "github.com/drone/drone/pkg/build/buildfile" "github.com/drone/drone/pkg/build/git" + "github.com/drone/drone/pkg/build/repo" "github.com/drone/drone/pkg/plugin/deploy" "github.com/drone/drone/pkg/plugin/notify" "github.com/drone/drone/pkg/plugin/publish" @@ -81,13 +82,13 @@ type Build struct { // Write adds all the steps to the build script, including // build commands, deploy and publish commands. -func (b *Build) Write(f *buildfile.Buildfile) { +func (b *Build) Write(f *buildfile.Buildfile, r *repo.Repo) { // append build commands b.WriteBuild(f) // write publish commands if b.Publish != nil { - b.Publish.Write(f) + b.Publish.Write(f, r) } // write deployment commands diff --git a/pkg/database/testing/testing.go b/pkg/database/testing/testing.go index 59bd3263a..3cca73927 100644 --- a/pkg/database/testing/testing.go +++ b/pkg/database/testing/testing.go @@ -115,10 +115,20 @@ func Setup() { Token: "789", GitlabToken: "789", Admin: false} + user4 := User{ + Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", + Name: "Rick El Toro", + Email: "rick@el.to.ro", + Gravatar: "c2180a539620d90d68eaeb848364f1c2", + Token: "987", + GitlabToken: "987", + Admin: false} + database.SaveUser(&user1) database.SaveUser(&user2) database.SaveUser(&user3) + database.SaveUser(&user4) // create dummy team data team1 := Team{ diff --git a/pkg/database/testing/users_test.go b/pkg/database/testing/users_test.go index 988e4aeb5..7d2385734 100644 --- a/pkg/database/testing/users_test.go +++ b/pkg/database/testing/users_test.go @@ -146,8 +146,8 @@ func TestListUsers(t *testing.T) { } // verify user count - if len(users) != 3 { - t.Errorf("Exepected %d users in database, got %d", 3, len(users)) + if len(users) != 4 { + t.Errorf("Exepected %d users in database, got %d", 4, len(users)) return } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 7adc6ab58..2af9150c9 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -102,7 +102,7 @@ func (h RepoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // The User must own the repository OR be a member // of the Team that owns the repository OR the repo // must not be private. - if repo.Private == false && user.ID != repo.UserID { + if repo.Private && user.ID != repo.UserID { if member, _ := database.IsMember(user.ID, repo.TeamID); !member { RenderNotFound(w) return diff --git a/pkg/handler/testing/handler_test.go b/pkg/handler/testing/handler_test.go new file mode 100644 index 000000000..9ea71824d --- /dev/null +++ b/pkg/handler/testing/handler_test.go @@ -0,0 +1,68 @@ +package testing + +import ( + "net/http" + "net/http/httptest" + "testing" + + . "github.com/drone/drone/pkg/database/testing" + "github.com/drone/drone/pkg/handler" + . "github.com/drone/drone/pkg/model" + + "github.com/bmizerany/pat" + . "github.com/smartystreets/goconvey/convey" +) + +func TestRepoHandler(t *testing.T) { + Setup() + defer Teardown() + + m := pat.New() + + Convey("Repo Handler", t, func() { + m.Get("/:host/:owner/:name", handler.RepoHandler(dummyUserRepo)) + Convey("Public repo can be viewed without login", func() { + req, err := http.NewRequest("GET", "/bitbucket.org/drone/test", nil) + So(err, ShouldBeNil) + rec := httptest.NewRecorder() + m.ServeHTTP(rec, req) + So(rec.Code, ShouldEqual, 200) + }) + Convey("Public repo can be viewed by another user", func() { + req, err := http.NewRequest("GET", "/bitbucket.org/drone/test", nil) + So(err, ShouldBeNil) + rec := httptest.NewRecorder() + setUserSession(rec, req, "cavepig@gmail.com") + m.ServeHTTP(rec, req) + So(rec.Code, ShouldEqual, 200) + }) + + Convey("Private repo can not be viewed without login", func() { + req, err := http.NewRequest("GET", "/github.com/drone/drone", nil) + So(err, ShouldBeNil) + rec := httptest.NewRecorder() + m.ServeHTTP(rec, req) + So(rec.Code, ShouldEqual, 303) + }) + Convey("Private repo can not be viewed by a non team member", func() { + req, err := http.NewRequest("GET", "/github.com/drone/drone", nil) + So(err, ShouldBeNil) + rec := httptest.NewRecorder() + setUserSession(rec, req, "rick@el.to.ro") + m.ServeHTTP(rec, req) + So(rec.Code, ShouldEqual, 404) + }) + }) +} + +func dummyUserRepo(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error { + return handler.RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) +} + +func setUserSession(w http.ResponseWriter, r *http.Request, username string) { + handler.SetCookie(w, r, "_sess", username) + resp := http.Response{Header: w.Header()} + for _, v := range resp.Cookies() { + r.AddCookie(v) + } +} diff --git a/pkg/plugin/deploy/ssh.go b/pkg/plugin/deploy/ssh.go index 59ab7d6d6..216e1fab5 100644 --- a/pkg/plugin/deploy/ssh.go +++ b/pkg/plugin/deploy/ssh.go @@ -73,7 +73,7 @@ func (s *SSH) Write(f *buildfile.Buildfile) { } if artifact { - scpCmd := "scp -o StrictHostKeyChecking=no -P %s ${ARTIFACT} %s" + scpCmd := "scp -o StrictHostKeyChecking=no -P %s -r ${ARTIFACT} %s" f.WriteCmd(fmt.Sprintf(scpCmd, host[1], host[0])) } diff --git a/pkg/plugin/deploy/ssh_test.go b/pkg/plugin/deploy/ssh_test.go index 53e25a29b..c2b82d492 100644 --- a/pkg/plugin/deploy/ssh_test.go +++ b/pkg/plugin/deploy/ssh_test.go @@ -85,7 +85,7 @@ func TestSSHOneArtifact(t *testing.T) { t.Errorf("Expect script to contains artifact") } - if !strings.Contains(bscr, "scp -o StrictHostKeyChecking=no -P 2212 ${ARTIFACT} user@test.example.com:/srv/app/location") { + if !strings.Contains(bscr, "scp -o StrictHostKeyChecking=no -P 2212 -r ${ARTIFACT} user@test.example.com:/srv/app/location") { t.Errorf("Expect script to contains scp command, got:\n%s", bscr) } } diff --git a/pkg/plugin/publish/npm.go b/pkg/plugin/publish/npm.go index 30b1a5b2a..d31879ed6 100644 --- a/pkg/plugin/publish/npm.go +++ b/pkg/plugin/publish/npm.go @@ -1 +1,73 @@ package publish + +import ( + "fmt" + + "github.com/drone/drone/pkg/build/buildfile" +) + +// use npm trick instead of running npm adduser that requires stdin +var npmLoginCmd = ` +cat < ~/.npmrc +_auth = $(echo "%s:%s" | tr -d "\r\n" | base64) +email = %s +EOF +` + +type NPM struct { + // The Email address used by NPM to connect + // and publish to a repository + Email string `yaml:"email,omitempty"` + + // The Username used by NPM to connect + // and publish to a repository + Username string `yaml:"username,omitempty"` + + // The Password used by NPM to connect + // and publish to a repository + Password string `yaml:"password,omitempty"` + + // Fails if the package name and version combination already + // exists in the registry. Overwrites when the "--force" flag is set. + Force bool `yaml:"force"` + + // The registry URL of custom npm repository + Registry string `yaml:"registry,omitempty"` + + // A folder containing the package.json file + Folder string `yaml:"folder,omitempty"` + + // Registers the published package with the given tag + Tag string `yaml:"tag,omitempty"` + + Branch string `yaml:"branch,omitempty"` +} + +func (n *NPM) Write(f *buildfile.Buildfile) { + + if len(n.Email) == 0 || len(n.Username) == 0 || len(n.Password) == 0 { + return + } + + npmPublishCmd := "npm publish %s" + + if n.Tag != "" { + npmPublishCmd += fmt.Sprintf(" --tag %s", n.Tag) + } + + if n.Force { + npmPublishCmd += " --force" + } + + f.WriteCmdSilent("echo 'publishing to NPM ...'") + + // Login to registry + f.WriteCmdSilent(fmt.Sprintf(npmLoginCmd, n.Username, n.Password, n.Email)) + + // Setup custom npm registry + if n.Registry != "" { + f.WriteCmdSilent(fmt.Sprintf("npm config set registry %s", n.Registry)) + } + + f.WriteCmd(fmt.Sprintf(npmPublishCmd, n.Folder)) +} \ No newline at end of file diff --git a/pkg/plugin/publish/npm_test.go b/pkg/plugin/publish/npm_test.go new file mode 100644 index 000000000..96c52581d --- /dev/null +++ b/pkg/plugin/publish/npm_test.go @@ -0,0 +1,91 @@ +package publish + +import ( + "strings" + "testing" + + "github.com/drone/drone/pkg/build/buildfile" + + "launchpad.net/goyaml" +) + +// emulate Build struct +type PublishToNPM struct { + Publish *Publish `yaml:"publish,omitempty"` +} + +var sampleYml1 = ` +publish: + npm: + username: foo + email: foo@example.com + password: bar +` + +var sampleYml2 = ` +publish: + npm: + username: foo + email: foo@example.com + password: bar + force: true +` + +var sampleYmlWithReg = ` +publish: + npm: + username: foo + email: foo@example.com + password: bar + registry: https://npm.example.com/me/ + folder: my-project/node-app/ + tag: 1.2.3 +` + +func setUpWithNPM(input string) (string, error) { + var buildStruct PublishToNPM + err := goyaml.Unmarshal([]byte(input), &buildStruct) + if err != nil { + return "", err + } + bf := buildfile.New() + buildStruct.Publish.Write(bf, nil) + return bf.String(), err +} + +func TestNPMPublish(t *testing.T) { + bscr, err := setUpWithNPM(sampleYml1) + if err != nil { + t.Fatalf("Can't unmarshal publish script: %s", err) + } + + if !strings.Contains(bscr, "npm publish") { + t.Error("Expect script to contain install command") + } +} + +func TestNPMForcePublish(t *testing.T) { + bscr, err := setUpWithNPM(sampleYml2) + if err != nil { + t.Fatalf("Can't unmarshal publish script: %s", err) + } + + if !strings.Contains(bscr, "npm publish --force") { + t.Error("Expect script to contain install command") + } +} + +func TestNPMPublishRegistry(t *testing.T) { + bscr, err := setUpWithNPM(sampleYmlWithReg) + if err != nil { + t.Fatalf("Can't unmarshal publish script: %s", err) + } + + if !strings.Contains(bscr, "npm config set registry https://npm.example.com/me/") { + t.Error("Expect script to contain npm config registry command") + } + + if !strings.Contains(bscr, "npm publish my-project/node-app/ --tag 1.2.3") { + t.Error("Expect script to contain npm publish command") + } +} diff --git a/pkg/plugin/publish/publish.go b/pkg/plugin/publish/publish.go index 2cfbf6deb..546be583e 100644 --- a/pkg/plugin/publish/publish.go +++ b/pkg/plugin/publish/publish.go @@ -2,6 +2,7 @@ package publish import ( "github.com/drone/drone/pkg/build/buildfile" + "github.com/drone/drone/pkg/build/repo" ) // Publish stores the configuration details @@ -11,16 +12,27 @@ type Publish struct { S3 *S3 `yaml:"s3,omitempty"` Swift *Swift `yaml:"swift,omitempty"` PyPI *PyPI `yaml:"pypi,omitempty"` + NPM *NPM `yaml:"npm,omitempty"` } -func (p *Publish) Write(f *buildfile.Buildfile) { - if p.S3 != nil { +func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) { + // S3 + if p.S3 != nil && (len(p.S3.Branch) == 0 || (len(p.S3.Branch) > 0 && r.Branch == p.S3.Branch)) { p.S3.Write(f) } - if p.Swift != nil { + + // Swift + if p.Swift != nil && (len(p.Swift.Branch) == 0 || (len(p.Swift.Branch) > 0 && r.Branch == p.Swift.Branch)) { p.Swift.Write(f) } - if p.PyPI != nil { + + // PyPI + if p.PyPI != nil && (len(p.PyPI.Branch) == 0 || (len(p.PyPI.Branch) > 0 && r.Branch == p.PyPI.Branch)) { p.PyPI.Write(f) } + + // NPM + if p.NPM != nil && (len(p.NPM.Branch) == 0 || (len(p.NPM.Branch) > 0 && r.Branch == p.NPM.Branch)) { + p.NPM.Write(f) + } } diff --git a/pkg/plugin/publish/pypi.go b/pkg/plugin/publish/pypi.go index faf28c6f6..998497838 100644 --- a/pkg/plugin/publish/pypi.go +++ b/pkg/plugin/publish/pypi.go @@ -2,6 +2,7 @@ package publish import ( "fmt" + "github.com/drone/drone/pkg/build/buildfile" ) @@ -10,17 +11,18 @@ var pypirc = ` cat < $HOME/.pypirc [distutils] index-servers = - pypi + %s -[pypi] +[%s] username:%s password:%s +%s EOF` var deployCmd = ` -if [ -z $_PYPI_SETUP_PY ] +if [ -n "$_PYPI_SETUP_PY" ] then - python $_PYPI_SETUP_PY sdist %s upload + python $_PYPI_SETUP_PY sdist %s upload -r %s if [ $? -ne 0 ] then echo "Deploy to PyPI failed - perhaps due to the version number not being incremented. Continuing..." @@ -31,27 +33,42 @@ fi ` type PyPI struct { - Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` - Formats []string `yaml:"formats,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Formats []string `yaml:"formats,omitempty"` + Repository string `yaml:"repository,omitempty"` + Branch string `yaml:"branch,omitempty"` } func (p *PyPI) Write(f *buildfile.Buildfile) { + var indexServer string + var repository string + if len(p.Username) == 0 || len(p.Password) == 0 { // nothing to do if the config is fundamentally flawed return } + + // Handle the setting a custom pypi server/repository + if len(p.Repository) == 0 { + indexServer = "pypi" + repository = "" + } else { + indexServer = "custom" + repository = fmt.Sprintf("repository:%s", p.Repository) + } + f.WriteCmdSilent("echo 'publishing to PyPI...'") // find the setup.py file f.WriteCmdSilent("_PYPI_SETUP_PY=$(find . -name 'setup.py')") // build the .pypirc file that pypi expects - f.WriteCmdSilent(fmt.Sprintf(pypirc, p.Username, p.Password)) + f.WriteCmdSilent(fmt.Sprintf(pypirc, indexServer, indexServer, p.Username, p.Password, repository)) formatStr := p.BuildFormatStr() // if we found the setup.py file use it to deploy - f.WriteCmdSilent(fmt.Sprintf(deployCmd, formatStr)) + f.WriteCmdSilent(fmt.Sprintf(deployCmd, formatStr, indexServer)) } func (p *PyPI) BuildFormatStr() string {