Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Shaun Duncan 2014-02-20 17:35:15 -05:00
commit 7bef94881b
97 changed files with 1963 additions and 805 deletions

View file

@ -1,10 +1,10 @@
image: mischief/docker-golang
image: go1.2
env:
- GOROOT=/usr/local/go
- GOPATH=/var/cache/drone
- PATH=$GOPATH/bin:$GOPATH/bin:$PATH
script:
- apt-get -y install libsqlite3-dev sqlite3 mercurial bzr 1> /dev/null 2> /dev/null
- sudo apt-get -y install libsqlite3-dev sqlite3 mercurial bzr 1> /dev/null 2> /dev/null
- make deps
- make
- make test
@ -21,5 +21,5 @@ publish:
bucket: downloads.drone.io
access_key: $AWS_KEY
secret_key: $AWS_SECRET
source: /tmp/drone.deb
source: deb/drone.deb
target: latest/

27
Dockerfile Normal file
View file

@ -0,0 +1,27 @@
FROM ubuntu:13.10
MAINTAINER Drone.io Team
RUN apt-get update
RUN apt-get install -y wget gcc make g++ build-essential ca-certificates mercurial git bzr libsqlite3-dev sqlite3
RUN wget https://go.googlecode.com/files/go1.2.src.tar.gz && tar zxvf go1.2.src.tar.gz && cd go/src && ./make.bash
ENV PATH $PATH:/go/bin:/gocode/bin
ENV GOPATH /gocode
RUN mkdir -p /gocode/src/github.com/drone
ADD . /gocode/src/github.com/drone/drone
WORKDIR /gocode/src/github.com/drone/drone
RUN make deps
RUN make
RUN make install
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/droned"]
CMD ["--port=:80", "--path=/var/lib/drone/drone.sqlite"]

View file

@ -2,19 +2,22 @@
all: embed build
deps:
[ -d $$GOPATH/src/code.google.com/p/go ] || hg clone -u default https://code.google.com/p/go $$GOPATH/src/code.google.com/p/go
[ -d $$GOPATH/src/github.com/dotcloud/docker ] || git clone git://github.com/dotcloud/docker $$GOPATH/src/github.com/dotcloud/docker
go get code.google.com/p/go.crypto/bcrypt
go get code.google.com/p/go.crypto/ssh
go get code.google.com/p/go.net/websocket
go get code.google.com/p/go.text/unicode/norm
#go get code.google.com/p/go/src/pkg/archive/tar
go get launchpad.net/goyaml
go get github.com/andybons/hipchat
go get github.com/bmizerany/pat
go get github.com/dchest/authcookie
go get github.com/dchest/passwordreset
go get github.com/dchest/uniuri
go get github.com/dotcloud/docker/archive
go get github.com/dotcloud/docker/pkg/term
go get github.com/dotcloud/docker/utils
#go get github.com/dotcloud/docker/archive
#go get github.com/dotcloud/docker/utils
#go get github.com/dotcloud/docker/pkg/term
go get github.com/drone/go-github/github
go get github.com/drone/go-bitbucket/bitbucket
go get github.com/GeertJohan/go.rice
@ -41,6 +44,7 @@ test:
go test -v github.com/drone/drone/pkg/channel
go test -v github.com/drone/drone/pkg/database
go test -v github.com/drone/drone/pkg/database/encrypt
go test -v github.com/drone/drone/pkg/database/migrate
go test -v github.com/drone/drone/pkg/database/testing
go test -v github.com/drone/drone/pkg/mail
go test -v github.com/drone/drone/pkg/model
@ -48,6 +52,7 @@ test:
install:
cp deb/drone/etc/init/drone.conf /etc/init/drone.conf
test -f /etc/default/drone || cp deb/drone/etc/default/drone /etc/default/drone
cd bin && install -t /usr/local/bin drone
cd bin && install -t /usr/local/bin droned
mkdir -p /var/lib/drone
@ -64,6 +69,7 @@ clean:
rm -rf usr/local/bin/drone
rm -rf usr/local/bin/droned
rm -rf drone.sqlite
rm -rf /tmp/drone.sqlite
# creates a debian package for drone
# to install `sudo dpkg -i drone.deb`
@ -75,4 +81,4 @@ dpkg:
dpkg-deb --build deb/drone
run:
bin/droned --port=":8080" --datasource="/tmp/drone.sqlite"
bin/droned --port=":8080" --datasource="drone.sqlite"

View file

@ -1,4 +1,7 @@
Drone is a Continuous Integration platform built on Docker
Drone is a [Continuous Integration](http://en.wikipedia.org/wiki/Continuous_integration) platform built on [Docker](https://www.docker.io/)
[![Build Status](http://beta.drone.io/github.com/drone/drone/status.png?branch=master)](http://beta.drone.io/github.com/drone/drone)
[![GoDoc](https://godoc.org/github.com/drone/drone?status.png)](https://godoc.org/github.com/drone/drone)
### System
@ -16,13 +19,25 @@ using the following commands:
```sh
$ wget http://downloads.drone.io/latest/drone.deb
$ dpkg -i drone.deb
$ sudo dpkg -i drone.deb
$ sudo start drone
```
Once Drone is running (by default on :80) navigate to **http://localhost:80/install**
and follow the steps in the setup wizard.
**IMPORTANT** You will also need a GitHub Client ID and Secret:
* Register a new application https://github.com/settings/applications
* Set the homepage URL to http://$YOUR_IP_ADDRESS/
* Set the callback URL to http://$YOUR_IP_ADDRESS/auth/login/github
* Copy the Client ID and Secret into the Drone admin console http://localhost:80/account/admin/settings
I'm working on a getting started video. Having issues with volume, but hopefully
you can still get a feel for the steps:
https://docs.google.com/file/d/0By8deR1ROz8memUxV0lTSGZPQUk
### Builds
Drone use a **.drone.yml** configuration file in the root of your
@ -35,7 +50,7 @@ env:
script:
- go build
- go test -v
service:
services:
- redis
notify:
email:
@ -60,7 +75,7 @@ image: go1.2 # same as bradrydzewski/go:1.2
Here is a list of our official images:
```sh
# these are the base images for all Drone containers.
# these two are base images. all Drone images are built on top of these
# these are BIG (~3GB) so make sure you have a FAST internet connection
docker pull bradrydzewski/ubuntu
docker pull bradrydzewski/base
@ -135,20 +150,33 @@ if you are using a custom Docker image.
Drone can launch database containers for your build:
```
service:
services:
- cassandra
- couchdb
- couchdb:1.0
- couchdb:1.4
- couchdb:1.5
- elasticsearch
- elasticsearch:0.20
- elasticsearch:0.90
- neo4j
- neo4j:1.9
- mongodb
- mongodb:2.2
- mongodb:2.4
- mysql
- mysql:5.5
- postgres
- postgres:9.1
- rabbitmq
- rabbitmq:3.2
- redis
- riak
- zookeeper
```
If you omit the version, Drone will launch the latest version of the database. (For example, if you set `mongodb`, Drone will launch MongoDB 2.4.)
**NOTE:** database and service containers are exposed over TCP connections and
have their own local IP address. If the **socat** utility is installed inside your
Docker image, Drone will automatically proxy localhost connections to the correct
@ -177,8 +205,8 @@ publish:
### Notifications
Drone can trigger email, hipchat and web hook notification at the completion
of your build:
Drone can trigger email, hipchat and web hook notification at the beginning and
completion of your build:
```
notify:
@ -192,11 +220,39 @@ notify:
hipchat:
room: support
token: 3028700e5466d375
token: 3028700e5466d375
on_started: true
on_success: true
on_failure: true
```
### Git Command Options
You can specify the `--depth` option of the `git clone` command (default value is `50`):
```
git:
depth: 1
```
### Params Injection
You can inject params into .drone.yml.
```
notify:
hipchat:
room: {{hipchatRoom}}
token: {{hipchatToken}}
on_started: true
on_success: true
on_failure: true
```
![params-injection](https://f.cloud.github.com/assets/1583973/2161187/2905077e-94c3-11e3-8499-a3844682c8af.png)
### Docs
Coming Soon to [drone.readthedocs.org](http://drone.readthedocs.org/)
* [drone.readthedocs.org](http://drone.readthedocs.org/) (Coming Soon)
* [GoDoc](http://godoc.org/github.com/drone/drone)

View file

@ -164,7 +164,7 @@ func run(path string) {
if len(*identity) != 0 {
key, err = ioutil.ReadFile(*identity)
if err != nil {
fmt.Printf("[Error] Could not find or read identity file %s\n", identity)
fmt.Printf("[Error] Could not find or read identity file %s\n", *identity)
os.Exit(1)
return
}

View file

@ -1074,3 +1074,6 @@ ul.account-radio-group li img {
padding: 20px;
margin-bottom: 30px;
}
.url {
word-break: break-all;
}

View file

@ -1260,4 +1260,8 @@ pre {
padding: 20px;
margin-bottom:30px;
}
}
}
.url {
word-break: break-all;
}

View file

@ -15,6 +15,7 @@ import (
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/database/migrate"
"github.com/drone/drone/pkg/handler"
)
@ -55,8 +56,9 @@ func main() {
// setup the database connection and register with the
// global database package.
func setupDatabase() {
// inform meddler we're using sqlite
// inform meddler and migration we're using sqlite
meddler.Default = meddler.SQLite
migrate.Driver = migrate.SQLite
// connect to the SQLite database
db, err := sql.Open(driver, datasource)
@ -65,6 +67,9 @@ func setupDatabase() {
}
database.Set(db)
migration := migrate.New(db)
migration.All().Migrate()
}
// setup routes for static assets. These assets may
@ -73,7 +78,18 @@ func setupDatabase() {
func setupStatic() {
box := rice.MustFindBox("assets")
http.Handle("/css/", http.FileServer(box.HTTPBox()))
http.Handle("/img/", http.FileServer(box.HTTPBox()))
// we need to intercept all attempts to serve images
// so that we can add a cache-control settings
var images = http.FileServer(box.HTTPBox())
http.HandleFunc("/img/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/img/build_") {
w.Header().Add("Cache-Control", "no-cache")
}
// serce images
images.ServeHTTP(w, r)
})
}
// setup routes for serving dynamic content.
@ -86,6 +102,8 @@ func setupHandlers() {
m.Post("/forgot", handler.ErrorHandler(handler.ForgotPost))
m.Get("/reset", handler.ErrorHandler(handler.Reset))
m.Post("/reset", handler.ErrorHandler(handler.ResetPost))
m.Get("/signup", handler.ErrorHandler(handler.SignUp))
m.Post("/signup", handler.ErrorHandler(handler.SignUpPost))
m.Get("/register", handler.ErrorHandler(handler.Register))
m.Post("/register", handler.ErrorHandler(handler.RegisterPost))
m.Get("/accept", handler.UserHandler(handler.TeamMemberAccept))
@ -145,8 +163,7 @@ func setupHandlers() {
m.Get("/:host/:owner/:name/commit/:commit/build/:label/out.txt", handler.RepoHandler(handler.BuildOut))
m.Get("/:host/:owner/:name/commit/:commit/build/:label", handler.RepoHandler(handler.CommitShow))
m.Get("/:host/:owner/:name/commit/:commit", handler.RepoHandler(handler.CommitShow))
m.Get("/:host/:owner/:name/tree/:branch/status.png", handler.ErrorHandler(handler.Badge))
m.Get("/:host/:owner/:name/tree/:branch", handler.RepoHandler(handler.RepoDashboard))
m.Get("/:host/:owner/:name/tree", handler.RepoHandler(handler.RepoDashboard))
m.Get("/:host/:owner/:name/status.png", handler.ErrorHandler(handler.Badge))
m.Get("/:host/:owner/:name/settings", handler.RepoAdminHandler(handler.RepoSettingsForm))
m.Get("/:host/:owner/:name/params", handler.RepoAdminHandler(handler.RepoParamsForm))
@ -165,8 +182,6 @@ func setupHandlers() {
// the first time a page is requested we should record
// the scheme and hostname.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// get the hostname and scheme
// our multiplexer is a bit finnicky and therefore requires
// us to strip any trailing slashes in order to correctly
// find and match a route.

View file

@ -0,0 +1,2 @@
/etc/init/drone.conf
/etc/default/drone

24
deb/drone/DEBIAN/postinst Executable file
View file

@ -0,0 +1,24 @@
#!/bin/sh
set -e
case "$1" in
abort-upgrade|abort-remove|abort-deconfigure|configure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
echo "Starting drone ..."
if [ -f /etc/init/drone.conf ]; then
if pidof /usr/local/bin/droned >/dev/null; then
service drone stop || exit $?
fi
service drone start && echo "Drone started."
fi
#DEBHELPER#
exit 0

26
deb/drone/DEBIAN/prerm Executable file
View file

@ -0,0 +1,26 @@
#!/bin/sh
set -e
set -u
case "$1" in
remove|remove-in-favour|deconfigure|deconfigure-in-favour)
if [ -f /etc/init/drone.conf ]; then
echo "Stopping drone ..."
service drone stop || exit $?
echo "Drone Stopped."
fi
;;
upgrade|failed-upgrade)
;;
*)
echo "prerm called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

View file

@ -0,0 +1,10 @@
# Upstart configuration file for droned.
# Command line options:
#
# -datasource="drone.sqlite":
# -driver="sqlite3":
# -path="":
# -port=":8080":
#
#DRONED_OPTS="--port=:80"

View file

@ -4,5 +4,9 @@ chdir /var/lib/drone
console log
script
droned --port=":80"
end script
DRONED_OPTS="--port=:80"
if [ -f /etc/default/$UPSTART_JOB ]; then
. /etc/default/$UPSTART_JOB
fi
droned $DRONED_OPTS
end script

View file

@ -32,15 +32,8 @@ var Logging = true
// New creates an instance of the Docker Client
func New() *Client {
c := &Client{}
c.proto = DEFAULTPROTOCOL
c.addr = DEFAULTUNIXSOCKET
// if the default socket doesn't exist then
// we'll try to connect to the default tcp address
if _, err := os.Stat(DEFAULTUNIXSOCKET); err != nil {
c.proto = "tcp"
c.addr = "0.0.0.0:4243"
}
c.setHost(DEFAULTUNIXSOCKET)
c.Images = &ImageService{c}
c.Containers = &ContainerService{c}
@ -76,6 +69,26 @@ var (
ErrBadRequest = errors.New("Bad Request")
)
func (c *Client) setHost(defaultUnixSocket string) {
c.proto = DEFAULTPROTOCOL
c.addr = defaultUnixSocket
if os.Getenv("DOCKER_HOST") != "" {
pieces := strings.Split(os.Getenv("DOCKER_HOST"), "://")
if len(pieces) == 2 {
c.proto = pieces[0]
c.addr = pieces[1]
}
} else {
// if the default socket doesn't exist then
// we'll try to connect to the default tcp address
if _, err := os.Stat(defaultUnixSocket); err != nil {
c.proto = "tcp"
c.addr = "0.0.0.0:4243"
}
}
}
// helper function used to make HTTP requests to the Docker daemon.
func (c *Client) do(method, path string, in, out interface{}) error {
// if data input is provided, serialize to JSON

View file

@ -0,0 +1,67 @@
package docker
import (
"io/ioutil"
"os"
"testing"
)
func TestHostFromEnv(t *testing.T) {
os.Setenv("DOCKER_HOST", "tcp://1.1.1.1:4243")
defer os.Setenv("DOCKER_HOST", "")
client := New()
if client.proto != "tcp" {
t.Fail()
}
if client.addr != "1.1.1.1:4243" {
t.Fail()
}
}
func TestInvalidHostFromEnv(t *testing.T) {
os.Setenv("DOCKER_HOST", "tcp:1.1.1.1:4243") // missing tcp:// prefix
defer os.Setenv("DOCKER_HOST", "")
client := New()
if client.addr == "1.1.1.1:4243" {
t.Fail()
}
}
func TestSocketHost(t *testing.T) {
// create temporary file to represent the docker socket
file, err := ioutil.TempFile("", "TestDefaultUnixHost")
if err != nil {
t.Fail()
}
file.Close()
defer os.Remove(file.Name())
client := &Client{}
client.setHost(file.Name())
if client.proto != "unix" {
t.Fail()
}
if client.addr != file.Name() {
t.Fail()
}
}
func TestDefaultTcpHost(t *testing.T) {
client := &Client{}
client.setHost("/tmp/missing_socket")
if client.proto != "tcp" {
t.Fail()
}
if client.addr != "0.0.0.0:4243" {
t.Fail()
}
}

View file

@ -59,7 +59,7 @@ func (c *ImageService) List() ([]*Images, error) {
// Create an image, either by pull it from the registry or by importing it.
func (c *ImageService) Create(image string) error {
return c.do("POST", fmt.Sprintf("/images/create?fromImage=%s"), nil, nil)
return c.do("POST", fmt.Sprintf("/images/create?fromImage=%s", image), nil, nil)
}
func (c *ImageService) Pull(image string) error {
@ -110,7 +110,7 @@ func (c *ImageService) Build(tag, dir string) error {
v := url.Values{}
v.Set("t", tag)
v.Set("q", "1")
//v.Set("rm", "1")
v.Set("rm", "1")
// url path
path := fmt.Sprintf("/build?%s", v.Encode())
@ -120,5 +120,5 @@ func (c *ImageService) Build(tag, dir string) error {
headers.Set("Content-Type", "application/tar")
// make the request
return c.stream("POST", path, body, nil, headers)
return c.stream("POST", path, body, os.Stdout, headers)
}

31
pkg/build/git/git.go Normal file
View file

@ -0,0 +1,31 @@
package git
const (
DefaultGitDepth = 50
)
// Git stores the configuration details for
// executing Git commands.
type Git struct {
// Depth options instructs git to create a shallow
// clone with a history truncated to the specified
// number of revisions.
Depth *int `yaml:"depth,omitempty"`
// The name of a directory to clone into.
// TODO this still needs to be implemented. this field is
// critical for forked Go projects, that need to clone
// to a specific repository.
Path string `yaml:"path,omitempty"`
}
// GitDepth returns GitDefaultDepth
// when Git.Depth is empty.
// GitDepth returns Git.Depth
// when it is not empty.
func GitDepth(g *Git) int {
if g == nil || g.Depth == nil {
return DefaultGitDepth
}
return *g.Depth
}

40
pkg/build/git/git_test.go Normal file
View file

@ -0,0 +1,40 @@
package git
import (
"testing"
)
func TestGitDepth(t *testing.T) {
var g *Git
var expected int
expected = DefaultGitDepth
g = nil
if actual := GitDepth(g); actual != expected {
t.Errorf("The result is invalid. [expected: %d][actual: %d]", expected, actual)
}
expected = DefaultGitDepth
g = &Git{}
if actual := GitDepth(g); actual != expected {
t.Errorf("The result is invalid. [expected: %d][actual: %d]", expected, actual)
}
expected = DefaultGitDepth
g = &Git{Depth: nil}
if actual := GitDepth(g); actual != expected {
t.Errorf("The result is invalid. [expected: %d][actual: %d]", expected, actual)
}
expected = 0
g = &Git{Depth: &expected}
if actual := GitDepth(g); actual != expected {
t.Errorf("The result is invalid. [expected: %d][actual: %d]", expected, actual)
}
expected = 1
g = &Git{Depth: &expected}
if actual := GitDepth(g); actual != expected {
t.Errorf("The result is invalid. [expected: %d][actual: %d]", expected, actual)
}
}

View file

@ -116,7 +116,7 @@ var services = map[string]*image{
// couchdb
"couchdb": {
Ports: []string{"5984"},
Tag: "bradrydzewski/couchdb:1.0",
Tag: "bradrydzewski/couchdb:1.5",
Name: "couchdb",
},
"couchdb:1.0": {

View file

@ -33,6 +33,9 @@ type Repo struct {
// will be cloned into (or copied to) inside the
// host system (Docker Container).
Dir string
// (optional) The depth of the `git clone` command.
Depth int
}
// IsRemote returns true if the Repository is located
@ -70,9 +73,9 @@ func (r *Repo) IsGit() bool {
return true
case strings.HasPrefix(r.Path, "ssh://git@"):
return true
case strings.HasPrefix(r.Path, "https://github.com/"):
case strings.HasPrefix(r.Path, "https://github"):
return true
case strings.HasPrefix(r.Path, "http://github.com"):
case strings.HasPrefix(r.Path, "http://github"):
return true
case strings.HasSuffix(r.Path, ".git"):
return true
@ -88,7 +91,6 @@ func (r *Repo) IsGit() bool {
//
// TODO we should also enable Mercurial projects and SVN projects
func (r *Repo) Commands() []string {
// get the branch. default to master
// if no branch exists.
branch := r.Branch
@ -97,7 +99,7 @@ func (r *Repo) Commands() []string {
}
cmds := []string{}
cmds = append(cmds, fmt.Sprintf("git clone --branch=%s %s %s", branch, r.Path, r.Dir))
cmds = append(cmds, fmt.Sprintf("git clone --depth=%d --recursive --branch=%s %s %s", r.Depth, branch, r.Path, r.Dir))
switch {
// if a specific commit is provided then we'll

View file

@ -1,12 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type AppFog struct {
}
func (a *AppFog) Write(f *buildfile.Buildfile) {
}

View file

@ -1,12 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type CloudControl struct {
}
func (c *CloudControl) Write(f *buildfile.Buildfile) {
}

View file

@ -1,12 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type CloudFoundry struct {
}
func (c *CloudFoundry) Write(f *buildfile.Buildfile) {
}

View file

@ -1,42 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
// Deploy stores the configuration details
// for deploying build artifacts when
// a Build has succeeded
type Deploy struct {
AppFog *AppFog `yaml:"appfog,omitempty"`
CloudControl *CloudControl `yaml:"cloudcontrol,omitempty"`
CloudFoundry *CloudFoundry `yaml:"cloudfoundry,omitempty"`
EngineYard *EngineYard `yaml:"engineyard,omitempty"`
Heroku *Heroku `yaml:"heroku,omitempty"`
Nodejitsu *Nodejitsu `yaml:"nodejitsu,omitempty"`
Openshift *Openshift `yaml:"openshift,omitempty"`
}
func (d *Deploy) Write(f *buildfile.Buildfile) {
if d.AppFog != nil {
d.AppFog.Write(f)
}
if d.CloudControl != nil {
d.CloudControl.Write(f)
}
if d.CloudFoundry != nil {
d.CloudFoundry.Write(f)
}
if d.EngineYard != nil {
d.EngineYard.Write(f)
}
if d.Heroku != nil {
d.Heroku.Write(f)
}
if d.Nodejitsu != nil {
d.Nodejitsu.Write(f)
}
if d.Openshift != nil {
d.Openshift.Write(f)
}
}

View file

@ -1,12 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type EngineYard struct {
}
func (e *EngineYard) Write(f *buildfile.Buildfile) {
}

View file

@ -1 +0,0 @@
package deployment

View file

@ -1,38 +0,0 @@
package deployment
import (
"fmt"
"github.com/drone/drone/pkg/build/buildfile"
)
type Heroku struct {
App string `yaml:"app,omitempty"`
Force bool `yaml:"force,omitempty"`
Branch string `yaml:"branch,omitempty"`
}
func (h *Heroku) Write(f *buildfile.Buildfile) {
// get the current commit hash
f.WriteCmdSilent("COMMIT=$(git rev-parse HEAD)")
// set the git user and email based on the individual
// that made the commit.
f.WriteCmdSilent("git config --global user.name $(git --no-pager log -1 --pretty=format:'%an')")
f.WriteCmdSilent("git config --global user.email $(git --no-pager log -1 --pretty=format:'%ae')")
// add heroku as a git remote
f.WriteCmd(fmt.Sprintf("git remote add heroku git@heroku.com:%s.git", h.App))
switch h.Force {
case true:
// this is useful when the there are artifacts generated
// by the build script, such as less files converted to css,
// that need to be deployed to Heroku.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'adding build artifacts'"))
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master --force"))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master"))
}
}

View file

@ -1,12 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type Nodejitsu struct {
}
func (n *Nodejitsu) Write(f *buildfile.Buildfile) {
}

View file

@ -1,12 +0,0 @@
package deployment
import (
"github.com/drone/drone/pkg/build/buildfile"
)
type Openshift struct {
}
func (o *Openshift) Write(f *buildfile.Buildfile) {
}

View file

@ -1 +0,0 @@
package deployment

View file

@ -1,85 +0,0 @@
package notification
import (
"fmt"
"net/smtp"
)
type Email struct {
Recipients []string `yaml:"recipients,omitempty"`
Success string `yaml:"on_success"`
Failure string `yaml:"on_failure"`
host string // smtp host address
port string // smtp host port
user string // smtp username for authentication
pass string // smtp password for authentication
from string // smtp email address. send from this address
}
// SetServer is a function that will set the SMTP
// server location and credentials
func (e *Email) SetServer(host, port, user, pass, from string) {
e.host = host
e.port = port
e.user = user
e.pass = pass
e.from = from
}
// Send will send an email, either success or failure,
// based on the Commit Status.
func (e *Email) Send(context *Context) error {
switch {
case context.Commit.Status == "Success" && e.Success != "never":
return e.sendSuccess(context)
case context.Commit.Status == "Failure" && e.Failure != "never":
return e.sendFailure(context)
}
return nil
}
// sendFailure sends email notifications to the list of
// recipients indicating the build failed.
func (e *Email) sendFailure(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendFailure(context.Repo.Slug, email, context); err != nil {
return err
}
}*/
return nil
}
// sendSuccess sends email notifications to the list of
// recipients indicating the build was a success.
func (e *Email) sendSuccess(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendSuccess(context.Repo.Slug, email, context); err != nil {
return err
}
}*/
return nil
}
// send is a simple helper function to format and
// send an email message.
func (e *Email) send(to, subject, body string) error {
// Format the raw email message body
raw := fmt.Sprintf(emailTemplate, e.from, to, subject, body)
auth := smtp.PlainAuth("", e.user, e.pass, e.host)
addr := fmt.Sprintf("%s:%s", e.host, e.port)
return smtp.SendMail(addr, auth, e.from, []string{to}, []byte(raw))
}
// text-template used to generate a raw Email message
var emailTemplate = `From: %s
To: %s
Subject: %s
MIME-version: 1.0
Content-Type: text/html; charset="UTF-8"
%s`

View file

@ -1,64 +0,0 @@
package notification
import (
"fmt"
"github.com/andybons/hipchat"
)
const (
startedMessage = "Building %s, commit %s, author %s"
successMessage = "<b>Success</b> %s, commit %s, author %s"
failureMessage = "<b>Failed</b> %s, commit %s, author %s"
)
type Hipchat struct {
Room string `yaml:"room,omitempty"`
Token string `yaml:"token,omitempty"`
Started bool `yaml:"on_started,omitempty"`
Success bool `yaml:"on_success,omitempty"`
Failure bool `yaml:"on_failure,omitempty"`
}
func (h *Hipchat) Send(context *Context) error {
switch {
case context.Commit.Status == "Started" && h.Started:
return h.sendStarted(context)
case context.Commit.Status == "Success" && h.Success:
return h.sendSuccess(context)
case context.Commit.Status == "Failure" && h.Failure:
return h.sendFailure(context)
}
return nil
}
func (h *Hipchat) sendStarted(context *Context) error {
msg := fmt.Sprintf(startedMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorYellow, hipchat.FormatHTML, msg)
}
func (h *Hipchat) sendFailure(context *Context) error {
msg := fmt.Sprintf(failureMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorRed, hipchat.FormatHTML, msg)
}
func (h *Hipchat) sendSuccess(context *Context) error {
msg := fmt.Sprintf(successMessage, context.Repo.Name, context.Commit.HashShort(), context.Commit.Author)
return h.send(hipchat.ColorGreen, hipchat.FormatHTML, msg)
}
// helper function to send Hipchat requests
func (h *Hipchat) send(color, format, message string) error {
c := hipchat.Client{AuthToken: h.Token}
req := hipchat.MessageRequest{
RoomId: h.Room,
From: "Drone",
Message: message,
Color: color,
MessageFormat: format,
Notify: true,
}
return c.PostMessage(req)
}

View file

@ -1 +0,0 @@
package notification

View file

@ -1,53 +0,0 @@
package notification
import (
"github.com/drone/drone/pkg/model"
)
// Context represents the context of an
// in-progress build request.
type Context struct {
// Global settings
Host string
// User that owns the repository
User *model.User
// Repository being built.
Repo *model.Repo
// Commit being built
Commit *model.Commit
}
type Sender interface {
Send(context *Context) error
}
// Notification stores the configuration details
// for notifying a user, or group of users,
// when their Build has completed.
type Notification struct {
Email *Email `yaml:"email,omitempty"`
Webhook *Webhook `yaml:"webhook,omitempty"`
Hipchat *Hipchat `yaml:"hipchat,omitempty"`
}
func (n *Notification) Send(context *Context) error {
// send email notifications
//if n.Email != nil && n.Email.Enabled {
// n.Email.Send(context)
//}
// send email notifications
if n.Webhook != nil {
n.Webhook.Send(context)
}
// send email notifications
if n.Hipchat != nil {
n.Hipchat.Send(context)
}
return nil
}

View file

@ -1,59 +0,0 @@
package notification
import (
"bytes"
"encoding/json"
"net/http"
"github.com/drone/drone/pkg/model"
)
type Webhook struct {
URL []string `yaml:"urls,omitempty"`
Success bool `yaml:"on_success,omitempty"`
Failure bool `yaml:"on_failure,omitempty"`
}
func (w *Webhook) Send(context *Context) error {
switch {
case context.Commit.Status == "Success" && w.Success:
return w.send(context)
case context.Commit.Status == "Failure" && w.Failure:
return w.send(context)
}
return nil
}
// helper function to send HTTP requests
func (w *Webhook) send(context *Context) error {
// data will get posted in this format
data := struct {
Owner *model.User `json:"owner"`
Repo *model.Repo `json:"repository"`
Commit *model.Commit `json:"commit"`
}{context.User, context.Repo, context.Commit}
// data json encoded
payload, err := json.Marshal(data)
if err != nil {
return err
}
// loop through and email recipients
for _, url := range w.URL {
go sendJson(url, payload)
}
return nil
}
// helper fuction to sent HTTP Post requests
// with JSON data as the payload.
func sendJson(url string, payload []byte) {
buf := bytes.NewBuffer(payload)
resp, err := http.Post(url, "application/json", buf)
if err != nil {
return
}
resp.Body.Close()
}

View file

@ -1 +0,0 @@
package notification

View file

@ -1 +0,0 @@
package publish

View file

@ -1 +0,0 @@
package publish

View file

@ -1 +0,0 @@
package publish

View file

@ -1 +0,0 @@
package publish

View file

@ -1 +0,0 @@
package publish

View file

@ -1 +0,0 @@
package publish

View file

@ -1,18 +0,0 @@
package publish
import (
"github.com/drone/drone/pkg/build/buildfile"
)
// Publish stores the configuration details
// for publishing build artifacts when
// a Build has succeeded
type Publish struct {
S3 *S3 `yaml:"s3,omitempty"`
}
func (p *Publish) Write(f *buildfile.Buildfile) {
if p.S3 != nil {
p.S3.Write(f)
}
}

View file

@ -1,2 +0,0 @@
package publish

View file

@ -1,85 +0,0 @@
package publish
import (
"fmt"
"strings"
"github.com/drone/drone/pkg/build/buildfile"
)
type S3 struct {
Key string `yaml:"access_key,omitempty"`
Secret string `yaml:"secret_key,omitempty"`
Bucket string `yaml:"bucket,omitempty"`
// us-east-1
// us-west-1
// us-west-2
// eu-west-1
// ap-southeast-1
// ap-southeast-2
// ap-northeast-1
// sa-east-1
Region string `yaml:"region,omitempty"`
// Indicates the files ACL, which should be one
// of the following:
// private
// public-read
// public-read-write
// authenticated-read
// bucket-owner-read
// bucket-owner-full-control
Access string `yaml:"acl,omitempty"`
// Copies the files from the specified directory.
// Regexp matching will apply to match multiple
// files
//
// Examples:
// /path/to/file
// /path/to/*.txt
// /path/to/*/*.txt
// /path/to/**
Source string `yaml:"source,omitempty"`
Target string `yaml:"target,omitempty"`
// Recursive uploads
Recursive bool `yaml:"recursive"`
Branch string `yaml:"branch,omitempty"`
}
func (s *S3) Write(f *buildfile.Buildfile) {
// install the AWS cli using PIP
f.WriteCmdSilent("[ -f /usr/bin/sudo ] || pip install awscli 1> /dev/null 2> /dev/null")
f.WriteCmdSilent("[ -f /usr/bin/sudo ] && sudo pip install awscli 1> /dev/null 2> /dev/null")
f.WriteEnv("AWS_ACCESS_KEY_ID", s.Key)
f.WriteEnv("AWS_SECRET_ACCESS_KEY", s.Secret)
// make sure a default region is set
if len(s.Region) == 0 {
s.Region = "us-east-1"
}
// make sure a default access is set
// let's be conservative and assume private
if len(s.Region) == 0 {
s.Region = "private"
}
// if the target starts with a "/" we need
// to remove it, otherwise we might adding
// a 3rd slash to s3://
if strings.HasPrefix(s.Target, "/") {
s.Target = s.Target[1:]
}
switch s.Recursive {
case true:
f.WriteCmd(fmt.Sprintf(`aws s3 cp %s s3://%s/%s --recursive --acl %s --region %s`, s.Source, s.Bucket, s.Target, s.Access, s.Region))
case false:
f.WriteCmd(fmt.Sprintf(`aws s3 cp %s s3://%s/%s --acl %s --region %s`, s.Source, s.Bucket, s.Target, s.Access, s.Region))
}
}

View file

@ -1,5 +0,0 @@
cobertura.go
coveralls.go
gocov.go
junit.go
phpunit.go

View file

@ -1,22 +1,25 @@
package script
import (
"bytes"
"fmt"
"io/ioutil"
"strings"
"launchpad.net/goyaml"
"github.com/drone/drone/pkg/build/buildfile"
"github.com/drone/drone/pkg/build/script/deployment"
"github.com/drone/drone/pkg/build/script/notification"
"github.com/drone/drone/pkg/build/script/publish"
"github.com/drone/drone/pkg/build/git"
"github.com/drone/drone/pkg/plugin/deploy"
"github.com/drone/drone/pkg/plugin/notify"
"github.com/drone/drone/pkg/plugin/publish"
)
func ParseBuild(data []byte) (*Build, error) {
func ParseBuild(data []byte, params map[string]string) (*Build, error) {
build := Build{}
// parse the build configuration file
err := goyaml.Unmarshal(data, &build)
err := goyaml.Unmarshal(injectParams(data, params), &build)
return &build, err
}
@ -26,7 +29,15 @@ func ParseBuildFile(filename string) (*Build, error) {
return nil, err
}
return ParseBuild(data)
return ParseBuild(data, nil)
}
// injectParams injects params into data.
func injectParams(data []byte, params map[string]string) []byte {
for k, v := range params {
data = bytes.Replace(data, []byte(fmt.Sprintf("{{%s}}", k)), []byte(v), -1)
}
return data
}
// Build stores the configuration details for
@ -51,9 +62,10 @@ type Build struct {
// linked to the build environment.
Services []string
Deploy *deployment.Deploy `yaml:"deploy,omitempty"`
Publish *publish.Publish `yaml:"publish,omitempty"`
Notifications *notification.Notification `yaml:"notify,omitempty"`
Deploy *deploy.Deploy `yaml:"deploy,omitempty"`
Publish *publish.Publish `yaml:"publish,omitempty"`
Notifications *notify.Notification `yaml:"notify,omitempty"`
Git *git.Git `yaml:"git,omitempty"`
}
// Write adds all the steps to the build script, including

View file

@ -0,0 +1,23 @@
package migrate
type Rev1 struct{}
var RenamePrivelegedToPrivileged = &Rev1{}
func (r *Rev1) Revision() int64 {
return 201402200603
}
func (r *Rev1) Up(op Operation) error {
_, err := op.RenameColumns("repos", map[string]string{
"priveleged": "privileged",
})
return err
}
func (r *Rev1) Down(op Operation) error {
_, err := op.RenameColumns("repos", map[string]string{
"privileged": "priveleged",
})
return err
}

View file

@ -0,0 +1,26 @@
package migrate
type Rev3 struct{}
var GitHubEnterpriseSupport = &Rev3{}
func (r *Rev3) Revision() int64 {
return 201402211147
}
func (r *Rev3) Up(op Operation) error {
_, err := op.AddColumn("settings", "github_domain VARCHAR(255)")
if err != nil {
return err
}
_, err = op.AddColumn("settings", "github_apiurl VARCHAR(255)")
op.Exec("update settings set github_domain=?", "github.com")
op.Exec("update settings set github_apiurl=?", "https://api.github.com")
return err
}
func (r *Rev3) Down(op Operation) error {
_, err := op.DropColumns("settings", []string{"github_domain", "github_apiurl"})
return err
}

View file

@ -0,0 +1,12 @@
package migrate
func (m *Migration) All() *Migration {
// List all migrations here
m.Add(RenamePrivelegedToPrivileged)
m.Add(GitHubEnterpriseSupport)
// m.Add(...)
// ...
return m
}

View file

@ -0,0 +1,215 @@
// Usage
// migrate.To(2)
// .Add(Version_1)
// .Add(Version_2)
// .Add(Version_3)
// .Exec(db)
//
// migrate.ToLatest()
// .Add(Version_1)
// .Add(Version_2)
// .Add(Version_3)
// .SetDialect(migrate.MySQL)
// .Exec(db)
//
// migrate.ToLatest()
// .Add(Version_1)
// .Add(Version_2)
// .Add(Version_3)
// .Backup(path)
// .Exec()
package migrate
import (
"database/sql"
"log"
)
const migrationTableStmt = `
CREATE TABLE IF NOT EXISTS migration (
revision NUMBER PRIMARY KEY
)
`
const migrationSelectStmt = `
SELECT revision FROM migration
WHERE revision = ?
`
const migrationSelectMaxStmt = `
SELECT max(revision) FROM migration
`
const insertRevisionStmt = `
INSERT INTO migration (revision) VALUES (?)
`
const deleteRevisionStmt = `
DELETE FROM migration where revision = ?
`
// Operation interface covers basic migration operations.
// Implementation details is specific for each database,
// see migrate/sqlite.go for implementation reference.
type Operation interface {
CreateTable(tableName string, args []string) (sql.Result, error)
RenameTable(tableName, newName string) (sql.Result, error)
DropTable(tableName string) (sql.Result, error)
AddColumn(tableName, columnSpec string) (sql.Result, error)
DropColumns(tableName string, columnsToDrop []string) (sql.Result, error)
RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error)
Exec(query string, args ...interface{}) (sql.Result, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
type Revision interface {
Up(op Operation) error
Down(op Operation) error
Revision() int64
}
type MigrationDriver struct {
Tx *sql.Tx
}
type Migration struct {
db *sql.DB
revs []Revision
}
var Driver func(tx *sql.Tx) Operation
func New(db *sql.DB) *Migration {
return &Migration{db: db}
}
// Add the Revision to the list of migrations.
func (m *Migration) Add(rev ...Revision) *Migration {
m.revs = append(m.revs, rev...)
return m
}
// Execute the full list of migrations.
func (m *Migration) Migrate() error {
var target int64
if len(m.revs) > 0 {
// get the last revision number in
// the list. This is what we'll
// migrate toward.
target = m.revs[len(m.revs)-1].Revision()
}
return m.MigrateTo(target)
}
// Execute all database migration until
// you are at the specified revision number.
// If the revision number is less than the
// current revision, then we will downgrade.
func (m *Migration) MigrateTo(target int64) error {
// make sure the migration table is created.
if _, err := m.db.Exec(migrationTableStmt); err != nil {
return err
}
// get the current revision
var current int64
m.db.QueryRow(migrationSelectMaxStmt).Scan(&current)
// already up to date
if current == target {
log.Println("Database already up-to-date.")
return nil
}
// should we downgrade?
if target < current {
return m.down(target, current)
}
// else upgrade
return m.up(target, current)
}
func (m *Migration) up(target, current int64) error {
// create the database transaction
tx, err := m.db.Begin()
if err != nil {
return err
}
op := Driver(tx)
// loop through and execute revisions
for _, rev := range m.revs {
if rev.Revision() > current && rev.Revision() <= target {
current = rev.Revision()
// execute the revision Upgrade.
if err := rev.Up(op); err != nil {
log.Printf("Failed to upgrade to Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
// update the revision number in the database
if _, err := tx.Exec(insertRevisionStmt, current); err != nil {
log.Printf("Failed to register Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
log.Printf("Successfully upgraded to Revision %v\n", current)
}
}
return tx.Commit()
}
func (m *Migration) down(target, current int64) error {
// create the database transaction
tx, err := m.db.Begin()
if err != nil {
return err
}
op := Driver(tx)
// reverse the list of revisions
revs := []Revision{}
for _, rev := range m.revs {
revs = append([]Revision{rev}, revs...)
}
// loop through the (reversed) list of
// revisions and execute.
for _, rev := range revs {
if rev.Revision() > target {
current = rev.Revision()
// execute the revision Upgrade.
if err := rev.Down(op); err != nil {
log.Printf("Failed to downgrade from Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
// update the revision number in the database
if _, err := tx.Exec(deleteRevisionStmt, current); err != nil {
log.Printf("Failed to unregistser Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
log.Printf("Successfully downgraded from Revision %v\n", current)
}
}
return tx.Commit()
}

30
pkg/database/migrate/migration Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
REV=$(date -u +%Y%m%d%H%M%S)
filename=$1
TAB="$(printf '\t')"
titleize() {
echo "$1" | sed -r -e "s/-|_/ /g" -e 's/\b(.)/\U\1/g' -e 's/ //g'
}
cat > ${REV}_$filename.go << EOF
package migrate
type rev${REV} struct{}
var $(titleize $filename) = &rev${REV}{}
func (r *rev$REV) Revision() int64 {
${TAB}return $REV
}
func (r *rev$REV) Up(op Operation) error {
${TAB}// Migration steps here
}
func (r *rev$REV) Down(op Operation) error {
${TAB}// Revert migration steps here
}
EOF

View file

@ -0,0 +1,283 @@
package migrate
import (
"database/sql"
"fmt"
"strings"
"github.com/dchest/uniuri"
_ "github.com/mattn/go-sqlite3"
)
type SQLiteDriver MigrationDriver
func SQLite(tx *sql.Tx) Operation {
return &SQLiteDriver{Tx: tx}
}
func (s *SQLiteDriver) Exec(query string, args ...interface{}) (sql.Result, error) {
return s.Tx.Exec(query, args...)
}
func (s *SQLiteDriver) Query(query string, args ...interface{}) (*sql.Rows, error) {
return s.Tx.Query(query, args...)
}
func (s *SQLiteDriver) QueryRow(query string, args ...interface{}) *sql.Row {
return s.Tx.QueryRow(query, args...)
}
func (s *SQLiteDriver) CreateTable(tableName string, args []string) (sql.Result, error) {
return s.Tx.Exec(fmt.Sprintf("CREATE TABLE %s (%s);", tableName, strings.Join(args, ", ")))
}
func (s *SQLiteDriver) RenameTable(tableName, newName string) (sql.Result, error) {
return s.Tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO %s;", tableName, newName))
}
func (s *SQLiteDriver) DropTable(tableName string) (sql.Result, error) {
return s.Tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s;", tableName))
}
func (s *SQLiteDriver) AddColumn(tableName, columnSpec string) (sql.Result, error) {
return s.Tx.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s;", tableName, columnSpec))
}
func (s *SQLiteDriver) DropColumns(tableName string, columnsToDrop []string) (sql.Result, error) {
var err error
var result sql.Result
if len(columnsToDrop) == 0 {
return nil, fmt.Errorf("No columns to drop.")
}
tableSQL, err := s.getDDLFromTable(tableName)
if err != nil {
return nil, err
}
columns, err := fetchColumns(tableSQL)
if err != nil {
return nil, err
}
columnNames := selectName(columns)
var preparedColumns []string
for k, column := range columnNames {
listed := false
for _, dropped := range columnsToDrop {
if column == dropped {
listed = true
break
}
}
if !listed {
preparedColumns = append(preparedColumns, columns[k])
}
}
if len(preparedColumns) == 0 {
return nil, fmt.Errorf("No columns match, drops nothing.")
}
// fetch indices for this table
oldSQLIndices, err := s.getDDLFromIndex(tableName)
if err != nil {
return nil, err
}
var oldIdxColumns [][]string
for _, idx := range oldSQLIndices {
idxCols, err := fetchColumns(idx)
if err != nil {
return nil, err
}
oldIdxColumns = append(oldIdxColumns, idxCols)
}
var indices []string
for k, idx := range oldSQLIndices {
listed := false
OIdxLoop:
for _, oidx := range oldIdxColumns[k] {
for _, cols := range columnsToDrop {
if oidx == cols {
listed = true
break OIdxLoop
}
}
}
if !listed {
indices = append(indices, idx)
}
}
// Rename old table, here's our proxy
proxyName := fmt.Sprintf("%s_%s", tableName, uniuri.NewLen(16))
if result, err := s.RenameTable(tableName, proxyName); err != nil {
return result, err
}
// Recreate table with dropped columns omitted
if result, err = s.CreateTable(tableName, preparedColumns); err != nil {
return result, err
}
// Move data from old table
if result, err = s.Tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT %s FROM %s;", tableName,
strings.Join(selectName(preparedColumns), ", "), proxyName)); err != nil {
return result, err
}
// Clean up proxy table
if result, err = s.DropTable(proxyName); err != nil {
return result, err
}
// Recreate Indices
for _, idx := range indices {
if result, err = s.Tx.Exec(idx); err != nil {
return result, err
}
}
return result, err
}
func (s *SQLiteDriver) RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error) {
var err error
var result sql.Result
tableSQL, err := s.getDDLFromTable(tableName)
if err != nil {
return nil, err
}
columns, err := fetchColumns(tableSQL)
if err != nil {
return nil, err
}
// We need a list of columns name to migrate data to the new table
var oldColumnsName = selectName(columns)
// newColumns will be used to create the new table
var newColumns []string
for k, column := range oldColumnsName {
added := false
for Old, New := range columnChanges {
if column == Old {
columnToAdd := strings.Replace(columns[k], Old, New, 1)
newColumns = append(newColumns, columnToAdd)
added = true
break
}
}
if !added {
newColumns = append(newColumns, columns[k])
}
}
// fetch indices for this table
oldSQLIndices, err := s.getDDLFromIndex(tableName)
if err != nil {
return nil, err
}
var idxColumns [][]string
for _, idx := range oldSQLIndices {
idxCols, err := fetchColumns(idx)
if err != nil {
return nil, err
}
idxColumns = append(idxColumns, idxCols)
}
var indices []string
for k, idx := range oldSQLIndices {
added := false
IdcLoop:
for _, oldIdx := range idxColumns[k] {
for Old, New := range columnChanges {
if oldIdx == Old {
indx := strings.Replace(idx, Old, New, 2)
indices = append(indices, indx)
added = true
break IdcLoop
}
}
}
if !added {
indices = append(indices, idx)
}
}
// Rename current table
proxyName := fmt.Sprintf("%s_%s", tableName, uniuri.NewLen(16))
if result, err := s.RenameTable(tableName, proxyName); err != nil {
return result, err
}
// Create new table with the new columns
if result, err = s.CreateTable(tableName, newColumns); err != nil {
return result, err
}
// Migrate data
if result, err = s.Tx.Exec(fmt.Sprintf("INSERT INTO %s SELECT %s FROM %s", tableName,
strings.Join(oldColumnsName, ", "), proxyName)); err != nil {
return result, err
}
// Clean up proxy table
if result, err = s.DropTable(proxyName); err != nil {
return result, err
}
for _, idx := range indices {
if result, err = s.Tx.Exec(idx); err != nil {
return result, err
}
}
return result, err
}
func (s *SQLiteDriver) getDDLFromTable(tableName string) (string, error) {
var sql string
query := `SELECT sql FROM sqlite_master WHERE type='table' and name=?;`
err := s.Tx.QueryRow(query, tableName).Scan(&sql)
if err != nil {
return "", err
}
return sql, nil
}
func (s *SQLiteDriver) getDDLFromIndex(tableName string) ([]string, error) {
var sqls []string
query := `SELECT sql FROM sqlite_master WHERE type='index' and tbl_name=?;`
rows, err := s.Tx.Query(query, tableName)
if err != nil {
return sqls, err
}
for rows.Next() {
var sql string
if err := rows.Scan(&sql); err != nil {
// This error came from autoindex, since its sql value is null,
// we want to continue.
if strings.Contains(err.Error(), "Scan pair: <nil> -> *string") {
continue
}
return sqls, err
}
sqls = append(sqls, sql)
}
if err := rows.Err(); err != nil {
return sqls, err
}
return sqls, nil
}

View file

@ -0,0 +1,520 @@
package migrate
import (
"database/sql"
"os"
"strings"
"testing"
"github.com/russross/meddler"
)
type Sample struct {
ID int64 `meddler:"id,pk"`
Imel string `meddler:"imel"`
Name string `meddler:"name"`
}
type RenameSample struct {
ID int64 `meddler:"id,pk"`
Email string `meddler:"email"`
Name string `meddler:"name"`
}
type AddColumnSample struct {
ID int64 `meddler:"id,pk"`
Imel string `meddler:"imel"`
Name string `meddler:"name"`
Url string `meddler:"url"`
Num int64 `meddler:"num"`
}
// ---------- revision 1
type revision1 struct{}
func (r *revision1) Up(op Operation) error {
_, err := op.CreateTable("samples", []string{
"id INTEGER PRIMARY KEY AUTOINCREMENT",
"imel VARCHAR(255) UNIQUE",
"name VARCHAR(255)",
})
return err
}
func (r *revision1) Down(op Operation) error {
_, err := op.DropTable("samples")
return err
}
func (r *revision1) Revision() int64 {
return 1
}
// ---------- end of revision 1
// ---------- revision 2
type revision2 struct{}
func (r *revision2) Up(op Operation) error {
_, err := op.RenameTable("samples", "examples")
return err
}
func (r *revision2) Down(op Operation) error {
_, err := op.RenameTable("examples", "samples")
return err
}
func (r *revision2) Revision() int64 {
return 2
}
// ---------- end of revision 2
// ---------- revision 3
type revision3 struct{}
func (r *revision3) Up(op Operation) error {
if _, err := op.AddColumn("samples", "url VARCHAR(255)"); err != nil {
return err
}
_, err := op.AddColumn("samples", "num INTEGER")
return err
}
func (r *revision3) Down(op Operation) error {
_, err := op.DropColumns("samples", []string{"num", "url"})
return err
}
func (r *revision3) Revision() int64 {
return 3
}
// ---------- end of revision 3
// ---------- revision 4
type revision4 struct{}
func (r *revision4) Up(op Operation) error {
_, err := op.RenameColumns("samples", map[string]string{
"imel": "email",
})
return err
}
func (r *revision4) Down(op Operation) error {
_, err := op.RenameColumns("samples", map[string]string{
"email": "imel",
})
return err
}
func (r *revision4) Revision() int64 {
return 4
}
// ---------- end of revision 4
// ---------- revision 5
type revision5 struct{}
func (r *revision5) Up(op Operation) error {
_, err := op.Exec(`CREATE INDEX samples_url_name_ix ON samples (url, name)`)
return err
}
func (r *revision5) Down(op Operation) error {
_, err := op.Exec(`DROP INDEX samples_url_name_ix`)
return err
}
func (r *revision5) Revision() int64 {
return 5
}
// ---------- end of revision 5
// ---------- revision 6
type revision6 struct{}
func (r *revision6) Up(op Operation) error {
_, err := op.RenameColumns("samples", map[string]string{
"url": "host",
})
return err
}
func (r *revision6) Down(op Operation) error {
_, err := op.RenameColumns("samples", map[string]string{
"host": "url",
})
return err
}
func (r *revision6) Revision() int64 {
return 6
}
// ---------- end of revision 6
// ---------- revision 7
type revision7 struct{}
func (r *revision7) Up(op Operation) error {
_, err := op.DropColumns("samples", []string{"host", "num"})
return err
}
func (r *revision7) Down(op Operation) error {
if _, err := op.AddColumn("samples", "host VARCHAR(255)"); err != nil {
return err
}
_, err := op.AddColumn("samples", "num INSTEGER")
return err
}
func (r *revision7) Revision() int64 {
return 7
}
// ---------- end of revision 7
// ---------- revision 8
type revision8 struct{}
func (r *revision8) Up(op Operation) error {
if _, err := op.AddColumn("samples", "repo_id INTEGER"); err != nil {
return err
}
_, err := op.AddColumn("samples", "repo VARCHAR(255)")
return err
}
func (r *revision8) Down(op Operation) error {
_, err := op.DropColumns("samples", []string{"repo", "repo_id"})
return err
}
func (r *revision8) Revision() int64 {
return 8
}
// ---------- end of revision 8
// ---------- revision 9
type revision9 struct{}
func (r *revision9) Up(op Operation) error {
_, err := op.RenameColumns("samples", map[string]string{
"repo": "repository",
})
return err
}
func (r *revision9) Down(op Operation) error {
_, err := op.RenameColumns("samples", map[string]string{
"repository": "repo",
})
return err
}
func (r *revision9) Revision() int64 {
return 9
}
// ---------- end of revision 9
var db *sql.DB
var testSchema = `
CREATE TABLE samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
imel VARCHAR(255) UNIQUE,
name VARCHAR(255)
);
`
var dataDump = []string{
`INSERT INTO samples (imel, name) VALUES ('test@example.com', 'Test Tester');`,
`INSERT INTO samples (imel, name) VALUES ('foo@bar.com', 'Foo Bar');`,
`INSERT INTO samples (imel, name) VALUES ('crash@bandicoot.io', 'Crash Bandicoot');`,
}
func TestMigrateCreateTable(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
mgr := New(db)
if err := mgr.Add(&revision1{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
sample := Sample{
ID: 1,
Imel: "test@example.com",
Name: "Test Tester",
}
if err := meddler.Save(db, "samples", &sample); err != nil {
t.Errorf("Can not save data: %q", err)
}
}
func TestMigrateRenameTable(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
mgr := New(db)
if err := mgr.Add(&revision1{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
loadFixture(t)
if err := mgr.Add(&revision2{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
sample := Sample{}
if err := meddler.QueryRow(db, &sample, `SELECT * FROM examples WHERE id = ?`, 2); err != nil {
t.Errorf("Can not fetch data: %q", err)
}
if sample.Imel != "foo@bar.com" {
t.Errorf("Column doesn't match. Expect: %s, got: %s", "foo@bar.com", sample.Imel)
}
}
type TableInfo struct {
CID int64 `meddler:"cid,pk"`
Name string `meddler:"name"`
Type string `meddler:"type"`
Notnull bool `meddler:"notnull"`
DfltValue interface{} `meddler:"dflt_value"`
PK bool `meddler:"pk"`
}
func TestMigrateAddRemoveColumns(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
mgr := New(db)
if err := mgr.Add(&revision1{}, &revision3{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var columns []*TableInfo
if err := meddler.QueryAll(db, &columns, `PRAGMA table_info(samples);`); err != nil {
t.Errorf("Can not access table info: %q", err)
}
if len(columns) < 5 {
t.Errorf("Expect length columns: %d\nGot: %d", 5, len(columns))
}
var row = AddColumnSample{
ID: 33,
Name: "Foo",
Imel: "foo@bar.com",
Url: "http://example.com",
Num: 42,
}
if err := meddler.Save(db, "samples", &row); err != nil {
t.Errorf("Can not save into database: %q", err)
}
if err := mgr.MigrateTo(1); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var another_columns []*TableInfo
if err := meddler.QueryAll(db, &another_columns, `PRAGMA table_info(samples);`); err != nil {
t.Errorf("Can not access table info: %q", err)
}
if len(another_columns) != 3 {
t.Errorf("Expect length columns = %d, got: %d", 3, len(columns))
}
}
func TestRenameColumn(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
mgr := New(db)
if err := mgr.Add(&revision1{}, &revision4{}).MigrateTo(1); err != nil {
t.Errorf("Can not migrate: %q", err)
}
loadFixture(t)
if err := mgr.MigrateTo(4); err != nil {
t.Errorf("Can not migrate: %q", err)
}
row := RenameSample{}
if err := meddler.QueryRow(db, &row, `SELECT * FROM samples WHERE id = 3;`); err != nil {
t.Errorf("Can not query database: %q", err)
}
if row.Email != "crash@bandicoot.io" {
t.Errorf("Expect %s, got %s", "crash@bandicoot.io", row.Email)
}
}
func TestMigrateExistingTable(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
if _, err := db.Exec(testSchema); err != nil {
t.Errorf("Can not create database: %q", err)
}
loadFixture(t)
mgr := New(db)
if err := mgr.Add(&revision4{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var rows []*RenameSample
if err := meddler.QueryAll(db, &rows, `SELECT * from samples;`); err != nil {
t.Errorf("Can not query database: %q", err)
}
if len(rows) != 3 {
t.Errorf("Expect rows length = %d, got %d", 3, len(rows))
}
if rows[1].Email != "foo@bar.com" {
t.Errorf("Expect email = %s, got %s", "foo@bar.com", rows[1].Email)
}
}
type sqliteMaster struct {
Sql interface{} `meddler:"sql"`
}
func TestIndexOperations(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
mgr := New(db)
// Migrate, create index
if err := mgr.Add(&revision1{}, &revision3{}, &revision5{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var esquel []*sqliteMaster
// Query sqlite_master, check if index is exists.
query := `SELECT sql FROM sqlite_master WHERE type='index' and tbl_name='samples'`
if err := meddler.QueryAll(db, &esquel, query); err != nil {
t.Errorf("Can not find index: %q", err)
}
indexStatement := `CREATE INDEX samples_url_name_ix ON samples (url, name)`
if string(esquel[1].Sql.([]byte)) != indexStatement {
t.Errorf("Can not find index")
}
// Migrate, rename indexed columns
if err := mgr.Add(&revision6{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var esquel1 []*sqliteMaster
if err := meddler.QueryAll(db, &esquel1, query); err != nil {
t.Errorf("Can not find index: %q", err)
}
indexStatement = `CREATE INDEX samples_host_name_ix ON samples (host, name)`
if string(esquel1[1].Sql.([]byte)) != indexStatement {
t.Errorf("Can not find index, got: %s", esquel[0])
}
if err := mgr.Add(&revision7{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var esquel2 []*sqliteMaster
if err := meddler.QueryAll(db, &esquel2, query); err != nil {
t.Errorf("Can not find index: %q", err)
}
if len(esquel2) != 1 {
t.Errorf("Expect row length equal to %d, got %d", 1, len(esquel2))
}
}
func TestColumnRedundancy(t *testing.T) {
defer tearDown()
if err := setUp(); err != nil {
t.Fatalf("Error preparing database: %q", err)
}
Driver = SQLite
migr := New(db)
if err := migr.Add(&revision1{}, &revision8{}, &revision9{}).Migrate(); err != nil {
t.Errorf("Can not migrate: %q", err)
}
var tableSql string
query := `SELECT sql FROM sqlite_master where type='table' and name='samples'`
if err := db.QueryRow(query).Scan(&tableSql); err != nil {
t.Errorf("Can not query sqlite_master: %q", err)
}
if !strings.Contains(tableSql, "repository ") {
t.Errorf("Expect column with name repository")
}
}
func setUp() error {
var err error
db, err = sql.Open("sqlite3", "migration_tests.sqlite")
return err
}
func tearDown() {
db.Close()
os.Remove("migration_tests.sqlite")
}
func loadFixture(t *testing.T) {
for _, sql := range dataDump {
if _, err := db.Exec(sql); err != nil {
t.Errorf("Can not insert into database: %q", err)
}
}
}

View file

@ -0,0 +1,32 @@
package migrate
import (
"fmt"
"strings"
)
func fetchColumns(sql string) ([]string, error) {
if !strings.HasPrefix(sql, "CREATE ") {
return []string{}, fmt.Errorf("Sql input is not a DDL statement.")
}
parenIdx := strings.Index(sql, "(")
return strings.Split(sql[parenIdx+1:strings.LastIndex(sql, ")")], ","), nil
}
func selectName(columns []string) []string {
var results []string
for _, column := range columns {
col := strings.SplitN(strings.Trim(column, " \n\t"), " ", 2)
results = append(results, col[0])
}
return results
}
func setForUpdate(left []string, right []string) string {
var results []string
for k, str := range left {
results = append(results, fmt.Sprintf("%s = %s", str, right[k]))
}
return strings.Join(results, ", ")
}

View file

@ -13,7 +13,7 @@ const repoTable = "repos"
// SQL Queries to retrieve a list of all repos belonging to a User.
const repoStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
public_key, private_key, params, timeout, privileged, created, updated, user_id, team_id
FROM repos
WHERE user_id = ? AND team_id = 0
ORDER BY slug ASC
@ -22,7 +22,7 @@ ORDER BY slug ASC
// SQL Queries to retrieve a list of all repos belonging to a Team.
const repoTeamStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
public_key, private_key, params, timeout, privileged, created, updated, user_id, team_id
FROM repos
WHERE team_id = ?
ORDER BY slug ASC
@ -31,7 +31,7 @@ ORDER BY slug ASC
// SQL Queries to retrieve a repo by id.
const repoFindStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
public_key, private_key, params, timeout, privileged, created, updated, user_id, team_id
FROM repos
WHERE id = ?
`
@ -39,7 +39,7 @@ WHERE id = ?
// SQL Queries to retrieve a repo by name.
const repoFindSlugStmt = `
SELECT id, slug, host, owner, name, private, disabled, disabled_pr, scm, url, username, password,
public_key, private_key, params, timeout, priveleged, created, updated, user_id, team_id
public_key, private_key, params, timeout, privileged, created, updated, user_id, team_id
FROM repos
WHERE slug = ?
`

View file

@ -49,7 +49,7 @@ insert into builds values (9, 3, 'node_0.80', 'Success', '2013-09-16 00:00:00','
-- insert default, dummy settings
insert into settings values (1,'','','','','','','','','','localhost:8080','http');
insert into settings values (1,'','','github.com','https://api.github.com','','','','','','','','localhost:8080','http',0);
-- add public & private keys to all repositories
@ -123,4 +123,4 @@ Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.849 sec
Results :
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0';
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0';

View file

@ -127,6 +127,7 @@ CREATE TABLE settings (
,smtp_password VARCHAR(1024)
,hostname VARCHAR(1024)
,scheme VARCHAR(5)
,open_invitations BOOLEAN
);
`
@ -194,5 +195,9 @@ func Load(db *sql.DB) error {
db.Exec(buildCommitIndex)
db.Exec(buildSlugIndex)
// migrations for backward compatibility
db.Exec("ALTER TABLE settings ADD COLUMN open_invitations BOOLEAN")
db.Exec("UPDATE settings SET open_invitations=0 WHERE open_invitations IS NULL")
return nil
}

View file

@ -51,7 +51,7 @@ CREATE TABLE repos (
,private BOOLEAN
,disabled BOOLEAN
,disabled_pr BOOLEAN
,priveleged BOOLEAN
,privileged BOOLEAN
,timeout INTEGER
,scm VARCHAR(25)
@ -103,6 +103,8 @@ CREATE TABLE settings (
id INTEGER PRIMARY KEY
,github_key VARCHAR(255)
,github_secret VARCHAR(255)
,github_domain VARCHAR(255)
,github_apiurl VARCHAR(255)
,bitbucket_key VARCHAR(255)
,bitbucket_secret VARCHAR(255)
,smtp_server VARCHAR(1024)
@ -112,6 +114,7 @@ CREATE TABLE settings (
,smtp_password VARCHAR(1024)
,hostname VARCHAR(1024)
,scheme VARCHAR(5)
,open_invitations BOOLEAN
);
CREATE UNIQUE INDEX member_uix ON members (team_id, user_id);

View file

@ -10,8 +10,8 @@ const settingsTable = "settings"
// SQL Queries to retrieve the system settings
const settingsStmt = `
SELECT id, github_key, github_secret, bitbucket_key, bitbucket_secret,
smtp_server, smtp_port, smtp_address, smtp_username, smtp_password, hostname, scheme
SELECT id, github_key, github_secret, github_domain, github_apiurl, bitbucket_key, bitbucket_secret,
smtp_server, smtp_port, smtp_address, smtp_username, smtp_password, hostname, scheme, open_invitations
FROM settings WHERE id = 1
`

View file

@ -7,6 +7,7 @@ import (
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/database/encrypt"
"github.com/drone/drone/pkg/database/migrate"
. "github.com/drone/drone/pkg/model"
_ "github.com/mattn/go-sqlite3"
@ -31,6 +32,7 @@ func init() {
// notify meddler that we are working with sqlite
meddler.Default = meddler.SQLite
migrate.Driver = migrate.SQLite
}
func Setup() {
@ -40,6 +42,9 @@ func Setup() {
// make sure all the tables and indexes are created
database.Set(db)
migration := migrate.New(db)
migration.All().Migrate()
// create dummy user data
user1 := User{
Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS",

View file

@ -28,6 +28,10 @@ func TestGetUser(t *testing.T) {
t.Errorf("Exepected Password %s, got %s", "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", u.Password)
}
if u.Token != "123" {
t.Errorf("Exepected Token %s, got %s", "123", u.Token)
}
if u.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", u.Name)
}
@ -60,6 +64,10 @@ func TestGetUserEmail(t *testing.T) {
t.Errorf("Exepected Password %s, got %s", "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", u.Password)
}
if u.Token != "123" {
t.Errorf("Exepected Token %s, got %s", "123", u.Token)
}
if u.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", u.Name)
}
@ -155,6 +163,10 @@ func TestListUsers(t *testing.T) {
t.Errorf("Exepected Password %s, got %s", "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", u.Password)
}
if u.Token != "123" {
t.Errorf("Exepected Token %s, got %s", "123", u.Token)
}
if u.Name != "Brad Rydzewski" {
t.Errorf("Exepected Name %s, got %s", "Brad Rydzewski", u.Name)
}

View file

@ -12,21 +12,21 @@ const userTable = "users"
// SQL Queries to retrieve a user by their unique database key
const userFindIdStmt = `
SELECT id, email, password, name, gravatar, created, updated, admin,
SELECT id, email, password, token, name, gravatar, created, updated, admin,
github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret
FROM users WHERE id = ?
`
// SQL Queries to retrieve a user by their email address
const userFindEmailStmt = `
SELECT id, email, password, name, gravatar, created, updated, admin,
SELECT id, email, password, token, name, gravatar, created, updated, admin,
github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret
FROM users WHERE email = ?
`
// SQL Queries to retrieve a list of all users
const userStmt = `
SELECT id, email, password, name, gravatar, created, updated, admin,
SELECT id, email, password, token, name, gravatar, created, updated, admin,
github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret
FROM users
ORDER BY name ASC

View file

@ -2,7 +2,6 @@ package handler
import (
"fmt"
"log"
"net/http"
"strconv"
"time"
@ -33,8 +32,7 @@ func AdminUserAdd(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "admin_users_add.html", &struct{ User *User }{u})
}
// Invite a user to join the system
func AdminUserInvite(w http.ResponseWriter, r *http.Request, u *User) error {
func UserInvite(w http.ResponseWriter, r *http.Request) error {
// generate the password reset token
email := r.FormValue("email")
token := authcookie.New(email, time.Now().Add(12*time.Hour), secret)
@ -57,15 +55,16 @@ func AdminUserInvite(w http.ResponseWriter, r *http.Request, u *User) error {
}{hostname, email, token}
// send the email message async
go func() {
if err := mail.SendActivation(email, data); err != nil {
log.Printf("error sending account activation email to %s. %s", email, err)
}
}()
go mail.SendActivation(email, data)
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
// Invite a user to join the system
func AdminUserInvite(w http.ResponseWriter, r *http.Request, u *User) error {
return UserInvite(w, r)
}
// Form to edit a user
func AdminUserEdit(w http.ResponseWriter, r *http.Request, u *User) error {
idstr := r.FormValue("id")
@ -140,7 +139,7 @@ func AdminUserDelete(w http.ResponseWriter, r *http.Request, u *User) error {
return nil
}
// Display a list of ALL users in the system
// Return an HTML form for the User to update the site settings.
func AdminSettings(w http.ResponseWriter, r *http.Request, u *User) error {
// get settings from database
settings := database.SettingsMust()
@ -153,7 +152,6 @@ func AdminSettings(w http.ResponseWriter, r *http.Request, u *User) error {
return RenderTemplate(w, "admin_settings.html", &data)
}
// Display a list of ALL users in the system
func AdminSettingsUpdate(w http.ResponseWriter, r *http.Request, u *User) error {
// get settings from database
settings := database.SettingsMust()
@ -169,6 +167,8 @@ func AdminSettingsUpdate(w http.ResponseWriter, r *http.Request, u *User) error
// update github settings
settings.GitHubKey = r.FormValue("GitHubKey")
settings.GitHubSecret = r.FormValue("GitHubSecret")
settings.GitHubDomain = r.FormValue("GitHubDomain")
settings.GitHubApiUrl = r.FormValue("GitHubApiUrl")
// update smtp settings
settings.SmtpServer = r.FormValue("SmtpServer")
@ -177,6 +177,8 @@ func AdminSettingsUpdate(w http.ResponseWriter, r *http.Request, u *User) error
settings.SmtpUsername = r.FormValue("SmtpUsername")
settings.SmtpPassword = r.FormValue("SmtpPassword")
settings.OpenInvitations = (r.FormValue("OpenInvitations") == "on")
// persist changes
if err := database.SaveSettings(settings); err != nil {
return RenderError(w, err, http.StatusBadRequest)
@ -243,6 +245,8 @@ func InstallPost(w http.ResponseWriter, r *http.Request) error {
settings := Settings{}
settings.Domain = r.FormValue("Domain")
settings.Scheme = r.FormValue("Scheme")
settings.GitHubApiUrl = "https://api.github.com";
settings.GitHubDomain = "github.com";
database.SaveSettings(&settings)
// add the user to the session object

View file

@ -47,7 +47,13 @@ func Index(w http.ResponseWriter, r *http.Request) error {
// Return an HTML form for the User to login.
func Login(w http.ResponseWriter, r *http.Request) error {
return RenderTemplate(w, "login.html", nil)
var settings, _ = database.GetSettings()
data := struct {
Settings *Settings
}{settings}
return RenderTemplate(w, "login.html", &data)
}
// Terminate the User session.
@ -70,6 +76,18 @@ func Reset(w http.ResponseWriter, r *http.Request) error {
return RenderTemplate(w, "reset.html", &struct{ Error string }{""})
}
// Return an HTML form for the User to signup.
func SignUp(w http.ResponseWriter, r *http.Request) error {
var settings, _ = database.GetSettings()
if settings == nil || !settings.OpenInvitations {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return nil
}
return RenderTemplate(w, "signup.html", nil)
}
// Return an HTML form to register for a new account. This
// page must be visited from a Signup email that contains
// a hash to verify the Email address is correct.
@ -138,15 +156,40 @@ func ResetPost(w http.ResponseWriter, r *http.Request) error {
}
// add the user to the session object
//session, _ := store.Get(r, "_sess")
//session.Values["username"] = user.Email
//session.Save(r, w)
SetCookie(w, r, "_sess", user.Email)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return nil
}
func SignUpPost(w http.ResponseWriter, r *http.Request) error {
// if self-registration is disabled we should display an
// error message to the user.
if !database.SettingsMust().OpenInvitations {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return nil
}
// generate the password reset token
email := r.FormValue("email")
token := authcookie.New(email, time.Now().Add(12*time.Hour), secret)
// get the hostname from the database for use in the email
hostname := database.SettingsMust().URL().String()
// data used to generate the email template
data := struct {
Host string
Email string
Token string
}{hostname, email, token}
// send the email message async
go mail.SendActivation(email, data)
return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK)
}
func RegisterPost(w http.ResponseWriter, r *http.Request) error {
// verify the token and extract the username
token := r.FormValue("token")
@ -156,9 +199,7 @@ func RegisterPost(w http.ResponseWriter, r *http.Request) error {
}
// set the email and name
user := User{}
user.SetEmail(email)
user.Name = r.FormValue("name")
user := NewUser(r.FormValue("name"), email)
// set the new password
password := r.FormValue("password")
@ -172,7 +213,7 @@ func RegisterPost(w http.ResponseWriter, r *http.Request) error {
}
// save to the database
if err := database.SaveUser(&user); err != nil {
if err := database.SaveUser(user); err != nil {
return err
}

View file

@ -48,8 +48,8 @@ func LinkGithub(w http.ResponseWriter, r *http.Request, u *User) error {
// github OAuth2 Data
var oauth = oauth2.Client{
RedirectURL: settings.URL().String() + "/auth/login/github",
AccessTokenURL: "https://github.com/login/oauth/access_token",
AuthorizationURL: "https://github.com/login/oauth/authorize",
AccessTokenURL: "https://" + settings.GitHubDomain + "/login/oauth/access_token",
AuthorizationURL: "https://" + settings.GitHubDomain + "/login/oauth/authorize",
ClientId: settings.GitHubKey,
ClientSecret: settings.GitHubSecret,
}
@ -72,6 +72,7 @@ func LinkGithub(w http.ResponseWriter, r *http.Request, u *User) error {
// create the client
client := github.New(token.AccessToken)
client.ApiUrl = settings.GitHubApiUrl
// get the user information
githubUser, err := client.Users.Current()

View file

@ -11,7 +11,7 @@ import (
// repository and an optional branch.
// TODO this needs to implement basic caching
func Badge(w http.ResponseWriter, r *http.Request) error {
branchParam := r.FormValue(":branch")
branchParam := r.FormValue("branch")
hostParam := r.FormValue(":host")
ownerParam := r.FormValue(":owner")
nameParam := r.FormValue(":name")

View file

@ -41,7 +41,7 @@ func (h UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// AdminHandler wraps the default http.HandlerFunc to include
// the currently authenticated User in the method signature,
// in addition to handling an error as the return value. It also
// verifies the user has Administrative priveleges.
// verifies the user has Administrative privileges.
type AdminHandler func(w http.ResponseWriter, r *http.Request, user *User) error
func (h AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -51,7 +51,7 @@ func (h AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// User MUST have administrative priveleges in order
// User MUST have administrative privileges in order
// to execute the handler.
if user.Admin == false {
RenderNotFound(w)

View file

@ -100,8 +100,13 @@ func Hook(w http.ResponseWriter, r *http.Request) error {
commit.SetAuthor(hook.Commits[0].Author.Email)
}
// get the github settings from the database
settings := database.SettingsMust()
// get the drone.yml file from GitHub
client := github.New(user.GithubToken)
client.ApiUrl = settings.GitHubApiUrl
content, err := client.Contents.FindRef(repo.Owner, repo.Name, ".drone.yml", commit.Hash)
if err != nil {
msg := "No .drone.yml was found in this repository. You need to add one.\n"
@ -122,7 +127,7 @@ func Hook(w http.ResponseWriter, r *http.Request) error {
}
// parse the build script
buildscript, err := script.ParseBuild(raw)
buildscript, err := script.ParseBuild(raw, repo.Params)
if err != nil {
msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n"
if err := saveFailedBuild(commit, msg); err != nil {
@ -216,8 +221,13 @@ func PullRequestHook(w http.ResponseWriter, r *http.Request) {
commit.Message = hook.PullRequest.Title
// label := p.PullRequest.Head.Labe
// get the github settings from the database
settings := database.SettingsMust()
// get the drone.yml file from GitHub
client := github.New(user.GithubToken)
client.ApiUrl = settings.GitHubApiUrl
content, err := client.Contents.FindRef(repo.Owner, repo.Name, ".drone.yml", commit.Hash) // TODO should this really be the hash??
if err != nil {
println(err.Error())
@ -233,7 +243,7 @@ func PullRequestHook(w http.ResponseWriter, r *http.Request) {
}
// parse the build script
buildscript, err := script.ParseBuild(raw)
buildscript, err := script.ParseBuild(raw, repo.Params)
if err != nil {
// TODO if the YAML is invalid we should create a commit record
// with an ERROR status so that the user knows why a build wasn't

View file

@ -173,7 +173,7 @@ func TeamMemberInvite(w http.ResponseWriter, r *http.Request, u *User) error {
}
// generate a token that is valid for 3 days to join the team
token := authcookie.New(team.Name, time.Now().Add(72*time.Hour), secret)
token := authcookie.New(strconv.Itoa(int(team.ID)), time.Now().Add(72*time.Hour), secret)
// hostname from settings
hostname := database.SettingsMust().URL().String()
@ -202,14 +202,14 @@ func TeamMemberInvite(w http.ResponseWriter, r *http.Request, u *User) error {
func TeamMemberAccept(w http.ResponseWriter, r *http.Request, u *User) error {
// get the team name from the token
token := r.FormValue("token")
teamName := authcookie.Login(token, secret)
if len(teamName) == 0 {
teamToken := authcookie.Login(token, secret)
teamId, err := strconv.Atoi(teamToken)
if err != nil || teamId == 0 {
return ErrInvalidTeamName
}
// get the team from the database
// TODO it might make more sense to use the ID in case the Slug changes
team, err := database.GetTeamSlug(teamName)
team, err := database.GetTeam(int64(teamId))
if err != nil {
return RenderError(w, err, http.StatusNotFound)
}
@ -222,6 +222,6 @@ func TeamMemberAccept(w http.ResponseWriter, r *http.Request, u *User) error {
}
// send the user to the dashboard
http.Redirect(w, r, "/dashboard/team/"+team.Name, http.StatusSeeOther)
http.Redirect(w, r, "/dashboard/team/"+team.Slug, http.StatusSeeOther)
return nil
}

View file

@ -15,7 +15,7 @@ import (
// Display a Repository dashboard.
func RepoDashboard(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
branch := r.FormValue(":branch")
branch := r.FormValue("branch")
// get a list of all branches
branches, err := database.ListBranches(repo.ID)
@ -54,6 +54,7 @@ func RepoDashboard(w http.ResponseWriter, r *http.Request, u *User, repo *Repo)
}
func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
settings := database.SettingsMust()
teams, err := database.ListTeams(u.ID)
if err != nil {
return err
@ -61,7 +62,8 @@ func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error {
data := struct {
User *User
Teams []*Team
}{u, teams}
Settings *Settings
}{u, teams, settings}
// if the user hasn't linked their GitHub account
// render a different template
if len(u.GithubToken) == 0 {
@ -82,12 +84,13 @@ func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error {
// create the GitHub client
client := github.New(u.GithubToken)
client.ApiUrl = settings.GitHubApiUrl
githubRepo, err := client.Repos.Find(owner, name)
if err != nil {
return err
}
repo, err := NewGitHubRepo(owner, name, githubRepo.Private)
repo, err := NewGitHubRepo(settings.GitHubDomain, owner, name, githubRepo.Private)
if err != nil {
return err
}
@ -274,7 +277,7 @@ func RepoDelete(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) err
// the user must confirm their password before deleting
password := r.FormValue("password")
if err := u.ComparePassword(password); err != nil {
return err
return RenderError(w, err, http.StatusBadRequest)
}
// delete the repo

View file

@ -122,7 +122,11 @@ func Send(msg *Message) error {
// format the raw email message body
body := fmt.Sprintf(emailTemplate, msg.Sender, msg.To, msg.Subject, msg.Body)
auth := smtp.PlainAuth("", s.SmtpUsername, s.SmtpPassword, s.SmtpServer)
var auth smtp.Auth
if len(s.SmtpUsername) > 0 {
auth = smtp.PlainAuth("", s.SmtpUsername, s.SmtpPassword, s.SmtpServer)
}
addr := fmt.Sprintf("%s:%s", s.SmtpServer, s.SmtpPort)
err = smtp.SendMail(addr, auth, msg.Sender, []string{msg.To}, []byte(body))

View file

@ -12,7 +12,6 @@ const (
)
const (
HostGithub = "github.com"
HostBitbucket = "bitbucket.org"
HostGoogle = "code.google.com"
HostCustom = "custom"
@ -25,8 +24,8 @@ const (
)
const (
githubRepoPattern = "git://github.com/%s/%s.git"
githubRepoPatternPrivate = "git@github.com:%s/%s.git"
githubRepoPattern = "git://%s/%s/%s.git"
githubRepoPatternPrivate = "git@%s:%s/%s.git"
bitbucketRepoPattern = "https://bitbucket.org/%s/%s.git"
bitbucketRepoPatternPrivate = "git@bitbucket.org:%s/%s.git"
)
@ -88,9 +87,9 @@ type Repo struct {
// before exceeding its timelimit and being killed.
Timeout int64 `meddler:"timeout" json:"timeout"`
// Indicates the build should be executed in priveleged
// Indicates the build should be executed in privileged
// mode. This could, for example, be used to run Docker in Docker.
Priveleged bool `meddler:"priveleged" json:"priveleged"`
Privileged bool `meddler:"privileged" json:"privileged"`
// Foreign keys signify the User that created
// the repository and team account linked to
@ -122,15 +121,15 @@ func NewRepo(host, owner, name, scm, url string) (*Repo, error) {
}
// Creates a new GitHub repository
func NewGitHubRepo(owner, name string, private bool) (*Repo, error) {
func NewGitHubRepo(domain, owner, name string, private bool) (*Repo, error) {
var url string
switch private {
case false:
url = fmt.Sprintf(githubRepoPattern, owner, name)
url = fmt.Sprintf(githubRepoPattern, domain, owner, name)
case true:
url = fmt.Sprintf(githubRepoPatternPrivate, owner, name)
url = fmt.Sprintf(githubRepoPatternPrivate, domain, owner, name)
}
return NewRepo(HostGithub, owner, name, ScmGit, url)
return NewRepo(domain, owner, name, ScmGit, url)
}
// Creates a new Bitbucket repository
@ -142,7 +141,7 @@ func NewBitbucketRepo(owner, name string, private bool) (*Repo, error) {
case true:
url = fmt.Sprintf(bitbucketRepoPatternPrivate, owner, name)
}
return NewRepo(HostGithub, owner, name, ScmGit, url)
return NewRepo(HostBitbucket, owner, name, ScmGit, url)
}
func (r *Repo) DefaultBranch() string {

View file

@ -17,6 +17,8 @@ type Settings struct {
// GitHub Consumer key and secret.
GitHubKey string `meddler:"github_key"`
GitHubSecret string `meddler:"github_secret"`
GitHubDomain string `meddler:"github_domain"`
GitHubApiUrl string `meddler:"github_apiurl"`
// Bitbucket Consumer Key and secret.
BitbucketKey string `meddler:"bitbucket_key"`
@ -27,6 +29,8 @@ type Settings struct {
// Scheme of the server, eg https
Scheme string `meddler:"scheme"`
OpenInvitations bool `meddler:"open_invitations"`
}
func (s *Settings) URL() *url.URL {

View file

@ -12,6 +12,7 @@ type Deploy struct {
CloudControl *CloudControl `yaml:"cloudcontrol,omitempty"`
CloudFoundry *CloudFoundry `yaml:"cloudfoundry,omitempty"`
EngineYard *EngineYard `yaml:"engineyard,omitempty"`
Git *Git `yaml:"git,omitempty"`
Heroku *Heroku `yaml:"heroku,omitempty"`
Nodejitsu *Nodejitsu `yaml:"nodejitsu,omitempty"`
Openshift *Openshift `yaml:"openshift,omitempty"`
@ -30,6 +31,9 @@ func (d *Deploy) Write(f *buildfile.Buildfile) {
if d.EngineYard != nil {
d.EngineYard.Write(f)
}
if d.Git != nil {
d.Git.Write(f)
}
if d.Heroku != nil {
d.Heroku.Write(f)
}

View file

@ -1 +1,38 @@
package deploy
import (
"fmt"
"github.com/drone/drone/pkg/build/buildfile"
)
type Git struct {
Target string `yaml:"target,omitempty"`
Force bool `yaml:"force,omitempty"`
Branch string `yaml:"branch,omitempty"`
}
func (g *Git) Write(f *buildfile.Buildfile) {
// get the current commit hash
f.WriteCmdSilent("COMMIT=$(git rev-parse HEAD)")
// set the git user and email based on the individual
// that made the commit.
f.WriteCmdSilent("git config --global user.name $(git --no-pager log -1 --pretty=format:'%an')")
f.WriteCmdSilent("git config --global user.email $(git --no-pager log -1 --pretty=format:'%ae')")
// add target as a git remote
f.WriteCmd(fmt.Sprintf("git remote add deploy %s", g.Target))
switch g.Force {
case true:
// this is useful when the there are artifacts generated
// by the build script, such as less files converted to css,
// that need to be deployed to git remote.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'add build artifacts'"))
f.WriteCmd(fmt.Sprintf("git push deploy $COMMIT:master --force"))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push deploy $COMMIT:master"))
}
}

View file

@ -1,30 +1,11 @@
package notify
import (
"fmt"
"net/smtp"
)
import "github.com/drone/drone/pkg/mail"
type Email struct {
Recipients []string `yaml:"recipients,omitempty"`
Success string `yaml:"on_success"`
Failure string `yaml:"on_failure"`
host string // smtp host address
port string // smtp host port
user string // smtp username for authentication
pass string // smtp password for authentication
from string // smtp email address. send from this address
}
// SetServer is a function that will set the SMTP
// server location and credentials
func (e *Email) SetServer(host, port, user, pass, from string) {
e.host = host
e.port = port
e.user = user
e.pass = pass
e.from = from
}
// Send will send an email, either success or failure,
@ -44,11 +25,11 @@ func (e *Email) Send(context *Context) error {
// recipients indicating the build failed.
func (e *Email) sendFailure(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendFailure(context.Repo.Slug, email, context); err != nil {
for _, email := range e.Recipients {
if err := mail.SendFailure(context.Repo.Name, email, context); err != nil {
return err
}
}*/
}
return nil
}
@ -56,30 +37,10 @@ func (e *Email) sendFailure(context *Context) error {
// recipients indicating the build was a success.
func (e *Email) sendSuccess(context *Context) error {
// loop through and email recipients
/*for _, email := range e.Recipients {
if err := mail.SendSuccess(context.Repo.Slug, email, context); err != nil {
for _, email := range e.Recipients {
if err := mail.SendSuccess(context.Repo.Name, email, context); err != nil {
return err
}
}*/
}
return nil
}
// send is a simple helper function to format and
// send an email message.
func (e *Email) send(to, subject, body string) error {
// Format the raw email message body
raw := fmt.Sprintf(emailTemplate, e.from, to, subject, body)
auth := smtp.PlainAuth("", e.user, e.pass, e.host)
addr := fmt.Sprintf("%s:%s", e.host, e.port)
return smtp.SendMail(addr, auth, e.from, []string{to}, []byte(raw))
}
// text-template used to generate a raw Email message
var emailTemplate = `From: %s
To: %s
Subject: %s
MIME-version: 1.0
Content-Type: text/html; charset="UTF-8"
%s`

View file

@ -8,7 +8,7 @@ import (
// in-progress build request.
type Context struct {
// Global settings
Settings *model.Settings
Host string
// User that owns the repository
User *model.User
@ -35,9 +35,9 @@ type Notification struct {
func (n *Notification) Send(context *Context) error {
// send email notifications
//if n.Email != nil && n.Email.Enabled {
// n.Email.Send(context)
//}
if n.Email != nil {
n.Email.Send(context)
}
// send email notifications
if n.Webhook != nil {

View file

@ -51,6 +51,14 @@ type S3 struct {
}
func (s *S3) Write(f *buildfile.Buildfile) {
// skip if AWS key or SECRET are empty. A good example for this would
// be forks building a project. S3 might be configured in the source
// repo, but not in the fork
if len(s.Key) == 0 || len(s.Secret) == 0 {
return
}
// install the AWS cli using PIP
f.WriteCmdSilent("[ -f /usr/bin/sudo ] || pip install awscli 1> /dev/null 2> /dev/null")
f.WriteCmdSilent("[ -f /usr/bin/sudo ] && sudo pip install awscli 1> /dev/null 2> /dev/null")
@ -65,8 +73,8 @@ func (s *S3) Write(f *buildfile.Buildfile) {
// make sure a default access is set
// let's be conservative and assume private
if len(s.Region) == 0 {
s.Region = "private"
if len(s.Access) == 0 {
s.Access = "private"
}
// if the target starts with a "/" we need

View file

@ -4,14 +4,15 @@ import (
"bytes"
"fmt"
bldr "github.com/drone/drone/pkg/build"
"github.com/drone/drone/pkg/build/git"
r "github.com/drone/drone/pkg/build/repo"
"github.com/drone/drone/pkg/build/script"
"github.com/drone/drone/pkg/build/script/notification"
"github.com/drone/drone/pkg/channel"
"github.com/drone/drone/pkg/database"
"github.com/drone/drone/pkg/mail"
. "github.com/drone/drone/pkg/model"
"github.com/drone/drone/pkg/plugin/notify"
"github.com/drone/go-github/github"
"log"
"path/filepath"
"time"
)
@ -93,7 +94,7 @@ func (b *BuildTask) execute() error {
settings, _ := database.GetSettings()
// notification context
context := &notification.Context{
context := &notify.Context{
Repo: b.Repo,
Commit: b.Commit,
Host: settings.URL().String(),
@ -104,6 +105,11 @@ func (b *BuildTask) execute() error {
b.Script.Notifications.Send(context)
}
// Send "started" notification to Github
if err := updateGitHubStatus(b.Repo, b.Commit); err != nil {
log.Printf("error updating github status: %s\n", err.Error())
}
// make sure a channel exists for the repository,
// the commit, and the commit output (TODO)
reposlug := fmt.Sprintf("%s/%s/%s", b.Repo.Host, b.Repo.Owner, b.Repo.Name)
@ -130,10 +136,19 @@ func (b *BuildTask) execute() error {
// execute the build
builder := bldr.Builder{}
builder.Build = b.Script
builder.Repo = &r.Repo{Path: b.Repo.URL, Branch: b.Commit.Branch, Commit: b.Commit.Hash, PR: b.Commit.PullRequest, Dir: filepath.Join("/var/cache/drone/src", b.Repo.Slug)}
builder.Repo = &r.Repo{Path: b.Repo.URL, Branch: b.Commit.Branch, Commit: b.Commit.Hash, PR: b.Commit.PullRequest, Dir: filepath.Join("/var/cache/drone/src", b.Repo.Slug), Depth: git.GitDepth(b.Script.Git)}
builder.Key = []byte(b.Repo.PrivateKey)
builder.Stdout = buf
builder.Timeout = 300 * time.Minute
defer func() {
// update the status of the commit using the
// GitHub status API.
if err := updateGitHubStatus(b.Repo, b.Commit); err != nil {
log.Printf("error updating github status: %s\n", err.Error())
}
}()
buildErr := builder.Run()
b.Build.Finished = time.Now().UTC()
@ -169,24 +184,11 @@ func (b *BuildTask) execute() error {
channel.SendJSON(commitslug, b.Build)
channel.Close(consoleslug)
// add the smtp address to the notificaitons
//if b.Script.Notifications != nil && b.Script.Notifications.Email != nil {
// b.Script.Notifications.Email.SetServer(settings.SmtpServer, settings.SmtpPort,
// settings.SmtpUsername, settings.SmtpPassword, settings.SmtpAddress)
//}
// send all "finished" notifications
if b.Script.Notifications != nil {
b.sendEmail(context) // send email from queue, not from inside /build/script package
b.Script.Notifications.Send(context)
}
// update the status of the commit using the
// GitHub status API.
if err := updateGitHubStatus(b.Repo, b.Commit); err != nil {
return err
}
return nil
}
@ -197,14 +199,14 @@ func updateGitHubStatus(repo *Repo, commit *Commit) error {
// convert from drone status to github status
var message, status string
switch status {
switch commit.Status {
case "Success":
status = "success"
message = "The build succeeded on drone.io"
case "Failure":
status = "failure"
message = "The build failed on drone.io"
case "Pending":
case "Started":
status = "pending"
message = "The build is pending on drone.io"
default:
@ -218,56 +220,17 @@ func updateGitHubStatus(repo *Repo, commit *Commit) error {
// get the user from the database
// since we need his / her GitHub token
user, err := database.GetUser(repo.UserID)
if err == nil {
if err != nil {
return err
}
client := github.New(user.GithubToken)
return client.Repos.CreateStatus(repo.Owner, repo.Name, status, settings.URL().String(), message, commit.Hash)
}
client.ApiUrl = settings.GitHubApiUrl;
func (t *BuildTask) sendEmail(c *notification.Context) error {
// make sure a notifications object exists
if t.Script.Notifications == nil && t.Script.Notifications.Email != nil {
return nil
}
var url string
url = settings.URL().String() + "/" + repo.Slug + "/commit/" + commit.Hash
switch {
case t.Commit.Status == "Success" && t.Script.Notifications.Email.Success != "never":
return t.sendSuccessEmail(c)
case t.Commit.Status == "Failure" && t.Script.Notifications.Email.Failure != "never":
return t.sendFailureEmail(c)
default:
println("sending nothing")
}
return nil
}
// sendFailure sends email notifications to the list of
// recipients indicating the build failed.
func (t *BuildTask) sendFailureEmail(c *notification.Context) error {
// loop through and email recipients
for _, email := range t.Script.Notifications.Email.Recipients {
if err := mail.SendFailure(t.Repo.Name, email, c); err != nil {
return err
}
}
return nil
}
// sendSuccess sends email notifications to the list of
// recipients indicating the build was a success.
func (t *BuildTask) sendSuccessEmail(c *notification.Context) error {
// loop through and email recipients
for _, email := range t.Script.Notifications.Email.Recipients {
if err := mail.SendSuccess(t.Repo.Name, email, c); err != nil {
return err
}
}
return nil
return client.Repos.CreateStatus(repo.Owner, repo.Name, status, url, message, commit.Hash)
}
type bufferWrapper struct {

View file

@ -1,4 +1,4 @@
{{ define "title" }}FAILURE{{end}}
{{ define "title" }}SUCCESS{{end}}
{{ define "content" }}
<!-- Callout Panel -->
@ -25,4 +25,4 @@
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; width: 99%; color: #333; margin: 0; padding: 0;">{{ .Commit.Message }}</td>
</tr>
</table>
{{ end }}
{{ end }}

View file

@ -33,6 +33,9 @@
{{ end }}
</select>
<input class="form-control form-control-xlarge" type="text" name="Domain" value="{{.Settings.Domain}}" />
<label class="checkbox">
Open invitations <input type="checkbox" name="OpenInvitations" {{ if .Settings.OpenInvitations }} checked {{ end }} />
</label>
</div>
<div class="form-group">
<div class="alert">GitHub OAuth Consumer Key and Secret</div>
@ -41,6 +44,11 @@
<input class="form-control form-control-large" type="text" name="GitHubKey" value="{{.Settings.GitHubKey}}" />
<input class="form-control form-control-large" type="password" name="GitHubSecret" value="{{.Settings.GitHubSecret}}" />
</div>
<label>GitHub Settings:</label>
<div>
<input class="form-control form-control-large" type="text" name="GitHubDomain" value="{{.Settings.GitHubDomain}}" />
<input class="form-control form-control-large" type="text" name="GitHubApiUrl" value="{{.Settings.GitHubApiUrl}}" />
</div>
</div>
<div class="form-group hide">
<div class="alert">Bitbucket OAuth Consumer Key and Secret.</div>

View file

@ -59,9 +59,9 @@
if (this.status == 200) {
var msg = "User Invitation was sent successfully";
if (this.responseText != "OK") {
msg = "Email is not currently enabled. In order to invite the user, you'll need to provide them the following link: " + this.responseText;
msg = "Email is not currently enabled. In order to invite the user, you'll need to provide them the following link:<br><span class='url'>" + this.responseText + "</span>";
}
$("#successAlert").text(msg);
$("#successAlert").html(msg);
$("#successAlert").show().removeClass("hide");
$('#submitButton').button('reset')
@ -75,4 +75,4 @@
return false;
}
</script>
{{ end }}
{{ end }}

View file

@ -38,7 +38,7 @@
<ul class="nav navbar-nav">
<li><a href="/dashboard">Home</a></li>
<li><a href="/account/user/profile">Settings</a></li>
<li><a href="/help">Help</a></li>
<li><a href="http://drone.readthedocs.org/en/latest/">Help</a></li>
<li><a class="btn btn-config" href="/account/admin/settings"><i class="fa fa-cogs"></i></a></li>
<li><a href="/new/github.com" class="btn">New Repository</a></li>
<li><a href="/logout" class="btn">Logout</a></li>

View file

@ -14,16 +14,18 @@
<div class="row">
<div class="col-xs-3">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="/github.com/drone/drone/settings">GitHub</a></li>
<li><a href="/github.com/drone/drone/secure">Bitbucket <small>(coming soon)</small></a></li>
</div><!-- ./col-xs-3 -->
<li class="active"><a href="/new/github.com">GitHub</a></li>
<li><a href="/new/bitbucket.org">Bitbucket <small>(coming soon)</small></a></li>
</ul>
</div><!-- ./col-xs-3 -->
<div class="col-xs-9" role="main">
<div class="col-xs-9" role="main">
<div class="alert">
Enter your repository details
<a class="btn btn-default pull-right" href="/auth/login/github" style="font-size: 18px;background:#f4f4f4;">Re-Link Account</a>
</div>
<form class="form-repo" method="POST" action="/new/github.com">
<input type="hidden" name="domain" autocomplete="off" value="{{.Settings.GitHubDomain}}">
<div class="field-group">
<div>
<label>GitHub Owner</label>
@ -84,7 +86,8 @@
if (this.status == 200) {
var name = $("input[name=name]").val()
var owner = $("input[name=owner]").val()
window.location.pathname = "/github.com/"+owner+"/"+name
var domain = $("input[name=domain]").val()
window.location.pathname = "/" + domain + "/"+owner+"/"+name
} else {
$("#failureAlert").text("Unable to setup the Repository");
$("#failureAlert").show().removeClass("hide");

View file

@ -14,11 +14,12 @@
<div class="row">
<div class="col-xs-3">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="/github.com/drone/drone/settings">GitHub</a></li>
<li><a href="/github.com/drone/drone/secure">Bitbucket <small>(coming soon)</small></a></li>
</div><!-- ./col-xs-3 -->
<li class="active"><a href="/new/github.com">GitHub</a></li>
<li><a href="/new/bitbucket.org">Bitbucket <small>(coming soon)</small></a></li>
</ul>
</div><!-- ./col-xs-3 -->
<div class="col-xs-9" role="main">
<div class="col-xs-9" role="main">
<div class="alert">Link Your GitHub Account
<a class="btn btn-primary pull-right" href="/auth/login/github" style="font-size: 18px;">Link Now</a>
</div>

View file

@ -2,20 +2,23 @@
{{ define "content" }}
<h1>Installation</h1>
<div>
<input class="form-control" type="text" name="name" placeholder="Full Name (e.g. John Smith)" autocomplete="off" spellcheck="off" />
<input class="form-control" type="text" name="email" placeholder="Email Address" autocomplete="off" spellcheck="off" style="border-top:0px;border-radius:0px;" />
<input class="form-control" type="password" name="password" placeholder="Password" />
</div>
<div>
<input type="hidden" name="Scheme" />
<input type="hidden" name="Domain" />
<input type="submit" value="Create Admin" />
</div>
<form action="install" method="post">
<div>
<input class="form-control" type="text" name="name" placeholder="Full Name (e.g. John Smith)" autocomplete="off" spellcheck="off" />
<input class="form-control" type="text" name="email" placeholder="Email Address" autocomplete="off" spellcheck="off" style="border-top:0px;border-radius:0px;" />
<input class="form-control" type="password" name="password" placeholder="Password" />
</div>
<div>
<input type="hidden" name="Scheme" />
<input type="hidden" name="Domain" />
<input type="submit" value="Create Admin" />
</div>
</form>
{{ end }}
{{ define "script" }}
<script>
$("input[name=Domain]").val(window.location.host)
$("input[name=Scheme]").val(window.location.protocol == "https:"?"https":"http")
</script>
{{ end }}

View file

@ -10,7 +10,11 @@
<input type="submit" value="Sign in" />
</div>
<div>
{{ if .Settings.OpenInvitations }}
<a href="/signup">request invitation</a> | <a href="/forgot">forgot password</a>
{{ else }}
<a href="/forgot">forgot password</a>
{{ end }}
</div>
{{ end }}

View file

@ -69,9 +69,9 @@
if (this.status == 200) {
var msg = "An invitation has been sent (via email) to join the Team.";
if (this.responseText != "OK") {
msg = "Email is not currently enabled. In order to invite this team member user, you'll need to provide them the following link: " + this.responseText;
msg = "Email is not currently enabled. In order to invite this team member user, you'll need to provide them the following link:<br><span class='url'>" + this.responseText + "</span>";
}
$("#successAlert").text(msg);
$("#successAlert").html(msg);
$("#successAlert").show().removeClass("hide");
$('#submitButton').button('reset')
} else {
@ -84,4 +84,4 @@
return false;
}
</script>
{{ end }}
{{ end }}

View file

@ -34,11 +34,11 @@
<div class="form-group">
<img class="pull-right" src="{{.Host}}/{{.Repo.Slug}}/status.png?branch=master">
<label>Badge, Markdown format</label>
<textarea class="form-control" rows="3">[![Build Status]({{.Host}}/{{.Repo.Name}}/status.png?branch=master)]({{.Host}}/{{.Repo.Name}}/tree/master)</textarea>
<textarea class="form-control" rows="3">[![Build Status]({{.Host}}/{{.Repo.Slug}}/status.png?branch=master)]({{.Host}}/{{.Repo.Slug}})</textarea>
</div>
<div class="form-group">
<label>Badge, HTML format</label>
<textarea class="form-control" rows="3">&lt;a href="{{.Host}}/{{.Repo.Slug}}/tree/master"&gt;&lt;img src="{{.Host}}/{{.Repo.Slug}}/status.png?branch=master" /&gt;&lt;/a&gt;</textarea>
<textarea class="form-control" rows="3">&lt;a href="{{.Host}}/{{.Repo.Slug}}"&gt;&lt;img src="{{.Host}}/{{.Repo.Slug}}/status.png?branch=master" /&gt;&lt;/a&gt;</textarea>
</div>
</form>
</div><!-- ./col-xs-9 -->
@ -47,4 +47,4 @@
{{ end }}
{{ define "script" }}
{{ end }}
{{ end }}

View file

@ -55,6 +55,66 @@
$(".timeago").timeago();
});
</script>
<script>
var re = /\u001B\[([0-9]+;?)*[Km]/g;
var styles = new Array();
var formatLine = function(s) {
// Check for newline and early exit?
s = s.replace(/</g, "&lt;");
s = s.replace(/>/g, "&gt;");
var final = "";
var current = 0;
while (m = re.exec(s)) {
var part = s.substring(current, m.index+1);
current = re.lastIndex;
var token = s.substr(m.index, re.lastIndex - m.index);
var code = token.substr(2, token.length-2);
var pre = "";
var post = "";
switch (code) {
case 'm':
case '0m':
var len = styles.length;
for (var i=0; i < len; i++) {
styles.pop();
post += "</span>"
}
break;
case '30;42m': pre = '<span style="color:black;background:lime">'; break;
case '36m':
case '36;1m': pre = '<span style="color:cyan;">'; break;
case '31m':
case '31;31m': pre = '<span style="color:red;">'; break;
case '33m':
case '33;33m': pre = '<span style="color:yellow;">'; break;
case '32m':
case '0;32m': pre = '<span style="color:lime;">'; break;
case '90m': pre = '<span style="color:gray;">'; break;
case 'K':
case '0K':
case '1K':
case '2K': break;
}
if (pre !== "") {
styles.push(pre);
}
final += part + pre + post;
}
var part = s.substring(current, s.length);
final += part;
return final;
};
</script>
<script>
{{ if .Build.IsRunning }}
var outputBox = document.getElementById('stdout');
@ -64,16 +124,11 @@
outputWS.onclose = function (e) { window.location.reload(); };
outputWS.onmessage = function (e) {
outputBox.innerHTML += formatLine(e.data);
//window.scrollTo(0, document.body.scrollHeight)
};
var re = /\u001B\[([0-9]+;?)*[Km]/g;
var styles = new Array();
var formatLine = function(s) {
return s;
window.scrollTo(0, document.body.scrollHeight)
};
{{ else }}
$.get("/{{ .Repo.Slug }}/commit/{{ .Commit.Hash }}/build/{{ .Build.Slug }}/out.txt", function( data ) {
$( "#stdout" ).html( data );
$( "#stdout" ).html(formatLine(data));
});
{{ end }}
</script>

View file

@ -19,7 +19,7 @@
<div class="container">
<div class="row">
<div class="col-xs-8" role="main">
<a class="btn btn-refresh hide" href="/{{.Repo.Slug}}/tree/{{.Branch}}">
<a class="btn btn-refresh hide" href="/{{.Repo.Slug}}/tree?branch={{.Branch}}">
<i class="fa fa-rotate-right"></i>
<span>0 new</span>
</a>
@ -33,7 +33,7 @@
<a href="/{{$repo.Slug}}/commit/{{.Hash}}">{{.HashShort}}</a>
<small class="timeago" title="{{.CreatedString}}"></small>
{{ if .PullRequest }}
<p>opened pull request <a href="/{{.Slug}}/commit/{{.Hash}}"># {{.PullRequest}}</a></p>
<p>opened pull request <a href="/{{$repo.Slug}}/commit/{{.Hash}}"># {{.PullRequest}}</a></p>
{{ else }}
<p>{{.Message}} &nbsp;</p>
{{ end }}
@ -49,7 +49,7 @@
<ul class="nav nav-pills nav-stacked nav-branches">
{{ range .Branches }}
<li{{ if eq $branch .Branch }} class="active"{{end}}>
<a href="/{{ $repo.Slug }}/tree/{{.Branch}}">
<a href="/{{ $repo.Slug }}/tree?branch={{.Branch}}">
<span class="btn btn-mini btn-{{.Status}} "></span>
<span>{{.Branch}}</span>
</a>
@ -78,4 +78,4 @@
$(".btn-refresh").show().removeClass("hide");
};
</script>
{{ end }}
{{ end }}

View file

@ -40,6 +40,7 @@
<div>
<input class="form-control" type="password" name="password" value="" />
</div>
<div class="alert alert-error hide" id="failureAlert"></div>
<div class="form-actions">
<input class="btn btn-danger" id="submitButton" type="submit" value="Delete Repository" />
<a class="btn btn-default" href="/{{.Repo.Slug}}/settings">Cancel</a>
@ -51,4 +52,27 @@
{{ end }}
{{ define "script" }}
{{ end }}
<script>
document.forms[0].onsubmit = function(event) {
$("#failureAlert").hide();
$('#submitButton').button('loading');
var form = event.target
var formData = new FormData(form);
xhr = new XMLHttpRequest();
xhr.open('POST', form.action);
xhr.onload = function() {
if (this.status == 400) {
$("#failureAlert").text("The password you entered was incorrect.");
$("#failureAlert").show().removeClass("hide");
$('#submitButton').button('reset')
} else {
window.location.href = "/dashboard";
}
};
xhr.send(formData);
return false;
}
</script>
{{ end }}

View file

@ -0,0 +1,45 @@
{{ define "title" }}Sign up · drone.io{{ end }}
{{ define "content" }}
<h1>Sign up</h1>
<form action="/signup" method="POST" role="form">
<div class="alert alert-success hide" id="successAlert"></div>
<div class="alert alert-error hide" id="failureAlert"></div>
<div>
<input type="text" name="email" placeholder="Email address" autocomplete="off" spellcheck="false" class="form-control only-child" />
</div>
<div>
<input type="submit" id="submitButton" value="Request invite" data-loading-text="Sending Invite .." />
</div>
</form>
{{ end }}
{{ define "script" }}
<script>
document.forms[0].onsubmit = function(event) {
$("#successAlert").hide();
$("#failureAlert").hide();
$('#submitButton').button('loading');
var form = event.target
var formData = new FormData(form);
xhr = new XMLHttpRequest();
xhr.open('POST', form.action);
xhr.onload = function() {
if (this.status == 200) {
$("#successAlert").text("User Invitation was sent successfully");
$("#successAlert").show().removeClass("hide");
$('#submitButton').button('reset')
} else {
$("#failureAlert").text("Failed to send Invitation Email. " + this.response);
$("#failureAlert").show().removeClass("hide");
$('#submitButton').button('reset')
};
};
xhr.send(formData);
return false;
}
</script>
{{ end }}

View file

@ -52,7 +52,7 @@
{{ if $commit.PullRequest }}
<p>opened pull request <a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}"># {{$commit.PullRequest}}</a></p>
{{ else }}
<p>commit <a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}">{{$commit.HashShort}}</a> to <a href="/{{$commit.Slug}}/tree/{{$commit.Branch}}">{{$commit.Branch}}</a> branch</p>
<p>commit <a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}">{{$commit.HashShort}}</a> to <a href="/{{$commit.Slug}}?branch={{$commit.Branch}}">{{$commit.Branch}}</a> branch</p>
{{ end }}
</h3>
</li>

View file

@ -43,14 +43,14 @@
<ul class="commit-list">
{{ range $commit := .Commits }}
<li>
<a href="/{{$commit.Name}}/commit/{{$commit.Hash}}" class="btn btn-{{$commit.Status}}"></a>
<a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}" class="btn btn-{{$commit.Status}}"></a>
<h3>
<a href="/{{$commit.Slug}}">{{$commit.Owner}} / {{$commit.Name}}</a>
<small class="timeago" title="{{$commit.CreatedString}}"></small>
{{ if $commit.PullRequest }}
<p>opened pull request <a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}"># {{$commit.PullRequest}}</a></p>
{{ else }}
<p>commit <a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}">{{$commit.HashShort}}</a> to <a href="/{{$commit.Slug}}/tree/{{$commit.Branch}}">{{$commit.Branch}}</a> branch</p>
<p>commit <a href="/{{$commit.Slug}}/commit/{{$commit.Hash}}">{{$commit.HashShort}}</a> to <a href="/{{$commit.Slug}}?branch={{$commit.Branch}}">{{$commit.Branch}}</a> branch</p>
{{ end }}
</h3>
</li>

View file

@ -45,6 +45,7 @@ func init() {
"forgot.html",
"forgot_sent.html",
"reset.html",
"signup.html",
"register.html",
"install.html",
@ -103,7 +104,7 @@ func init() {
// HACK: choose which base template to use FOR THE RECORD I
// don't really like this, but it works for now.
var baseTemplate = base
if i < 7 {
if i < 8 {
baseTemplate = form
}