mirror of
https://github.com/harness/drone.git
synced 2025-05-07 03:00:17 +08:00

This allows the same SHA to have different builds on different branches, each separately viewable. This is useful for expressing a "pipeline" in terms of branches, e.g. a commit starts on branch A and progress through B and C to master, with the build script switching on branch name. Previously viewing each build would arbitrarily choose which branch's commit to show.
243 lines
6.6 KiB
Go
243 lines
6.6 KiB
Go
package queue
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/drone/drone/pkg/build/git"
|
|
r "github.com/drone/drone/pkg/build/repo"
|
|
"github.com/drone/drone/pkg/channel"
|
|
"github.com/drone/drone/pkg/database"
|
|
. "github.com/drone/drone/pkg/model"
|
|
"github.com/drone/drone/pkg/plugin/notify"
|
|
"github.com/drone/go-github/github"
|
|
"io"
|
|
"log"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
type worker struct {
|
|
runner BuildRunner
|
|
}
|
|
|
|
// work is a function that will infinitely
|
|
// run in the background waiting for tasks that
|
|
// it can pull off the queue and execute.
|
|
func (w *worker) work(queue <-chan *BuildTask) {
|
|
var task *BuildTask
|
|
for {
|
|
// get work item (pointer) from the queue
|
|
task = <-queue
|
|
if task == nil {
|
|
continue
|
|
}
|
|
|
|
// execute the task
|
|
w.execute(task)
|
|
}
|
|
}
|
|
|
|
// execute will execute the build task and persist
|
|
// the results to the datastore.
|
|
func (w *worker) execute(task *BuildTask) error {
|
|
// we need to be sure that we can recover
|
|
// from any sort panic that could occur
|
|
// to avoid brining down the entire application
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
task.Build.Finished = time.Now().UTC()
|
|
task.Commit.Finished = time.Now().UTC()
|
|
task.Build.Duration = task.Build.Finished.Unix() - task.Build.Started.Unix()
|
|
task.Commit.Duration = task.Build.Finished.Unix() - task.Build.Started.Unix()
|
|
task.Commit.Status = "Error"
|
|
task.Build.Status = "Error"
|
|
database.SaveBuild(task.Build)
|
|
database.SaveCommit(task.Commit)
|
|
}
|
|
}()
|
|
|
|
// update commit and build status
|
|
task.Commit.Status = "Started"
|
|
task.Build.Status = "Started"
|
|
task.Build.Started = time.Now().UTC()
|
|
task.Commit.Started = time.Now().UTC()
|
|
|
|
// persist the commit to the database
|
|
if err := database.SaveCommit(task.Commit); err != nil {
|
|
return err
|
|
}
|
|
|
|
// persist the build to the database
|
|
if err := database.SaveBuild(task.Build); err != nil {
|
|
return err
|
|
}
|
|
|
|
// get settings
|
|
settings, _ := database.GetSettings()
|
|
|
|
// notification context
|
|
context := ¬ify.Context{
|
|
Repo: task.Repo,
|
|
Commit: task.Commit,
|
|
Host: settings.URL().String(),
|
|
}
|
|
|
|
// send all "started" notifications
|
|
if task.Script.Notifications != nil {
|
|
task.Script.Notifications.Send(context)
|
|
}
|
|
|
|
// Send "started" notification to Github
|
|
if err := updateGitHubStatus(task.Repo, task.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", task.Repo.Host, task.Repo.Owner, task.Repo.Name)
|
|
commitslug := fmt.Sprintf("%s/%s/%s/commit/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Branch, task.Commit.Hash)
|
|
consoleslug := fmt.Sprintf("%s/%s/%s/commit/%s/%s/builds/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Branch, task.Commit.Hash, task.Build.Slug)
|
|
channel.Create(reposlug)
|
|
channel.Create(commitslug)
|
|
channel.CreateStream(consoleslug)
|
|
|
|
// notify the channels that the commit and build started
|
|
channel.SendJSON(reposlug, task.Commit)
|
|
channel.SendJSON(commitslug, task.Build)
|
|
|
|
var buf = &bufferWrapper{channel: consoleslug}
|
|
|
|
// append private parameters to the environment
|
|
// variable section of the .drone.yml file, iff
|
|
// this is not a pull request (for security purposes)
|
|
if task.Repo.Params != nil && len(task.Commit.PullRequest) == 0 {
|
|
for k, v := range task.Repo.Params {
|
|
task.Script.Env = append(task.Script.Env, k+"="+v)
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
// update the status of the commit using the
|
|
// GitHub status API.
|
|
if err := updateGitHubStatus(task.Repo, task.Commit); err != nil {
|
|
log.Printf("error updating github status: %s\n", err.Error())
|
|
}
|
|
}()
|
|
|
|
// execute the build
|
|
passed, buildErr := w.runBuild(task, buf)
|
|
|
|
task.Build.Finished = time.Now().UTC()
|
|
task.Commit.Finished = time.Now().UTC()
|
|
task.Build.Duration = task.Build.Finished.UnixNano() - task.Build.Started.UnixNano()
|
|
task.Commit.Duration = task.Build.Finished.UnixNano() - task.Build.Started.UnixNano()
|
|
task.Commit.Status = "Success"
|
|
task.Build.Status = "Success"
|
|
task.Build.Stdout = buf.buf.String()
|
|
|
|
// if exit code != 0 set to failure
|
|
if passed {
|
|
task.Commit.Status = "Failure"
|
|
task.Build.Status = "Failure"
|
|
if buildErr != nil && task.Build.Stdout == "" {
|
|
// TODO: If you wanted to have very friendly error messages, you could do that here
|
|
task.Build.Stdout = buildErr.Error() + "\n"
|
|
}
|
|
}
|
|
|
|
// persist the build to the database
|
|
if err := database.SaveBuild(task.Build); err != nil {
|
|
return err
|
|
}
|
|
|
|
// persist the commit to the database
|
|
if err := database.SaveCommit(task.Commit); err != nil {
|
|
return err
|
|
}
|
|
|
|
// notify the channels that the commit and build finished
|
|
channel.SendJSON(reposlug, task.Commit)
|
|
channel.SendJSON(commitslug, task.Build)
|
|
channel.Close(consoleslug)
|
|
|
|
// send all "finished" notifications
|
|
if task.Script.Notifications != nil {
|
|
task.Script.Notifications.Send(context)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) runBuild(task *BuildTask, buf io.Writer) (bool, error) {
|
|
repo := &r.Repo{
|
|
Name: task.Repo.Slug,
|
|
Path: task.Repo.URL,
|
|
Branch: task.Commit.Branch,
|
|
Commit: task.Commit.Hash,
|
|
PR: task.Commit.PullRequest,
|
|
Dir: filepath.Join("/var/cache/drone/src", task.Repo.Slug),
|
|
Depth: git.GitDepth(task.Script.Git),
|
|
}
|
|
|
|
return w.runner.Run(
|
|
task.Script,
|
|
repo,
|
|
[]byte(task.Repo.PrivateKey),
|
|
buf,
|
|
)
|
|
}
|
|
|
|
// updateGitHubStatus is a helper function that will send
|
|
// the build status to GitHub using the Status API.
|
|
// see https://github.com/blog/1227-commit-status-api
|
|
func updateGitHubStatus(repo *Repo, commit *Commit) error {
|
|
|
|
// convert from drone status to github status
|
|
var message, status string
|
|
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 "Started":
|
|
status = "pending"
|
|
message = "The build is pending on drone.io"
|
|
default:
|
|
status = "error"
|
|
message = "The build errored on drone.io"
|
|
}
|
|
|
|
// get the system settings
|
|
settings, _ := database.GetSettings()
|
|
|
|
// get the user from the database
|
|
// since we need his / her GitHub token
|
|
user, err := database.GetUser(repo.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := github.New(user.GithubToken)
|
|
client.ApiUrl = settings.GitHubApiUrl
|
|
|
|
var url string
|
|
url = settings.URL().String() + "/" + repo.Slug + "/commit/" + commit.Branch + "/" + commit.Hash
|
|
|
|
return client.Repos.CreateStatus(repo.Owner, repo.Name, status, url, message, commit.Hash)
|
|
}
|
|
|
|
type bufferWrapper struct {
|
|
buf bytes.Buffer
|
|
|
|
// name of the channel
|
|
channel string
|
|
}
|
|
|
|
func (b *bufferWrapper) Write(p []byte) (n int, err error) {
|
|
n, err = b.buf.Write(p)
|
|
channel.SendBytes(b.channel, p)
|
|
return
|
|
}
|