diff --git a/.gitignore b/.gitignore index daf913b..6b9b092 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ _testmain.go *.exe *.test *.prof + +drone-pypi diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..f90d01c --- /dev/null +++ b/DOCS.md @@ -0,0 +1,19 @@ +Use the PyPI plugin to deploy a Python package to a PyPI server. + +* **repository** - The repository name (optional) +* **username** - The username to login with (optional) +* **password** - A password to login with (optional) +* **distributions** - A list of distribution types to deploy (optional) + +The following is an example configuration for your .drone.yml: + +```yaml +deploy: + pypi: + repository: https://pypi.python.org/pypi + username: guido + password: secret + distributions: + - sdist + - bdist_wheel +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..84dd62c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:3.2 + +RUN apk add -U \ + ca-certificates \ + py-pip \ + python \ + && rm -rf /var/cache/apk/* \ + && pip install --no-cache-dir --upgrade \ + pip \ + setuptools + +ADD drone-pypi /bin/ + +ENTRYPOINT ["/bin/drone-pypi"] diff --git a/README.md b/README.md index 22a71fb..371482e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ # drone-pypi + Drone plugin for publishing to the Python package index + +## Usage + +Upload a source distribution to PyPI + +```sh +./drone-pypi < + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..2d9a7dd --- /dev/null +++ b/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/drone/drone-plugin-go/plugin" +) + +type Params struct { + Distributions []string `json:"distributions"` + Password *string `json:"password,omitempty"` + Repository *string `json:"repository,omitempty"` + Username *string `json:"username,omitempty"` +} + +func main() { + w := plugin.Workspace{} + v := Params{} + plugin.Param("workspace", &w) + plugin.Param("vargs", &v) + plugin.MustParse() + + err := deploy(&w, &v) + if err != nil { + log.Fatal(err) + } +} + +func deploy(w *plugin.Workspace, v *Params) error { + err := createConfig(v) + if err != nil { + return err + } + err = uploadDist(w, v) + if err != nil { + return err + } + return nil +} + +func createConfig(v *Params) error { + f, err := os.Create(path.Join(os.Getenv("HOME"), ".pypirc")) + if err != nil { + return err + } + defer f.Close() + buf := bufio.NewWriter(f) + err = v.WriteConfig(buf) + if err != nil { + return err + } + buf.Flush() + return nil +} + +func uploadDist(w *plugin.Workspace, v *Params) error { + cmd, err := v.Upload() + if err != nil { + return err + } + cmd.Dir = w.Path + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fmt.Println("$", strings.Join(cmd.Args, " ")) + err = cmd.Run() + if err != nil { + return err + } + return nil +} + +// WriteConfig writes a .pypirc to a supplied io.Writer. +func (v *Params) WriteConfig(w io.Writer) error { + repository := "https://pypi.python.org/pypi" + if v.Repository != nil { + repository = *v.Repository + } + username := "guido" + if v.Username != nil { + username = *v.Username + } + password := "secret" + if v.Password != nil { + password = *v.Password + } + _, err := io.WriteString(w, fmt.Sprintf(`[distutils] +index-servers = + pypi + +[pypi] +repository: %s +username: %s +password: %s +`, repository, username, password)) + return err +} + +// Upload creates a setuptools upload command. +func (v *Params) Upload() (*exec.Cmd, error) { + distributions := []string{"sdist"} + if len(v.Distributions) > 0 { + distributions = v.Distributions + } + args := []string{"python", "setup.py"} + for i := range distributions { + args = append(args, distributions[i]) + } + args = append(args, "upload") + args = append(args, "-r") + args = append(args, "pypi") + return command(args) +} + +// Command builds a command using a variable length argument list. +func command(args []string) (*exec.Cmd, error) { + name := args[0] + cmd := &exec.Cmd{ + Path: name, + Args: args, + } + if filepath.Base(name) == name { + lp, err := exec.LookPath(name) + if err != nil { + return nil, err + } + cmd.Path = lp + } + return cmd, nil +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..4ded95f --- /dev/null +++ b/main_test.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/drone/drone-plugin-go/plugin" +) + +func TestDeploy(t *testing.T) { + w := plugin.Workspace{ + Path: os.Getenv("DRONE_PYPI_PATH"), + } + repository := os.Getenv("DRONE_PYPI_REPOSITORY") + username := os.Getenv("DRONE_PYPI_USERNAME") + password := os.Getenv("DRONE_PYPI_PASSWORD") + v := Params{ + Repository: &repository, + Username: &username, + Password: &password, + Distributions: strings.Split(os.Getenv("DRONE_PYPI_DISTRIBUTIONS"), " "), + } + if w.Path == "" { + t.Skip("DRONE_PYPI_PATH not set") + } + err := deploy(&w, &v) + if err != nil { + t.Error(err) + } +} + +func sPtr(s string) *string { + return &s +} + +func TestConfig(t *testing.T) { + testdata := []struct { + repository *string + username *string + password *string + exp string + }{ + { + nil, + nil, + nil, + `[distutils] +index-servers = + pypi + +[pypi] +repository: https://pypi.python.org/pypi +username: guido +password: secret +`, + }, + { + sPtr("https://pypi.example.com"), + nil, + nil, + `[distutils] +index-servers = + pypi + +[pypi] +repository: https://pypi.example.com +username: guido +password: secret +`, + }, + { + nil, + sPtr("jqhacker"), + sPtr("supersecret"), + `[distutils] +index-servers = + pypi + +[pypi] +repository: https://pypi.python.org/pypi +username: jqhacker +password: supersecret +`, + }, + } + for i, data := range testdata { + v := Params{ + Repository: data.repository, + Username: data.username, + Password: data.password, + Distributions: []string{}, + } + var b bytes.Buffer + v.WriteConfig(&b) + if b.String() != data.exp { + t.Errorf("Case %d: Expected %s, got %s\n", i, data.exp, b.String()) + } + } +} + +func TestUpload(t *testing.T) { + testdata := []struct { + distributions []string + exp []string + }{ + { + []string{}, + []string{"python", "setup.py", "sdist", "upload", "-r", "pypi"}, + }, + { + []string{"sdist", "bdist_wheel"}, + []string{"python", "setup.py", "sdist", "bdist_wheel", "upload", "-r", "pypi"}, + }, + } + for i, data := range testdata { + v := Params{Distributions: data.distributions} + c, err := v.Upload() + if err != nil { + t.Error(err) + } + if len(c.Args) != len(data.exp) { + t.Errorf("Case %d: Expected %d, got %d", i, len(data.exp), len(c.Args)) + } + for i := range c.Args { + if c.Args[i] != data.exp[i] { + t.Errorf("Case %d: Expected %s, got %s", i, strings.Join(data.exp, " "), strings.Join(c.Args, " ")) + } + } + } +}