Skip to content

Commit 956865a

Browse files
author
Lukas Matt
committed
Replace webhook build by agent construct
This will enable us to restart tests in future and a void broken builds after restarting the server
1 parent fa0f1a4 commit 956865a

4 files changed

Lines changed: 277 additions & 213 deletions

File tree

build.go

Lines changed: 260 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,282 @@
1818
package main
1919

2020
import (
21+
"fmt"
22+
"net/http"
23+
"strings"
24+
"encoding/json"
25+
"golang.org/x/oauth2"
26+
"github.com/google/go-github/github"
27+
"io/ioutil"
28+
"context"
29+
"time"
2130
"github.com/jinzhu/gorm"
31+
_ "github.com/jinzhu/gorm/dialects/sqlite"
32+
)
33+
34+
const (
35+
STATUS_ERROR = "error"
36+
STATUS_FAIL = "failure"
37+
STATUS_PENDING = "pending"
38+
STATUS_SUCCESS = "success"
39+
40+
BUILD_NOT_STARTED = 0
41+
BUILD_PENDING = 1
42+
BUILD_FINISHED = 2
2243
)
2344

2445
type Build struct {
2546
gorm.Model
26-
RepoID int
47+
RepoID uint
2748
Matrix string
2849
TravisType string
2950
TravisRequestID int64
3051
TravisRepositoryID int64
3152
PRUser string
3253
PRRepo string
3354
PRSha string
55+
Status int `gorm:"default:0"`
3456

3557
Repo Repo
3658
}
3759

60+
type Builds []Build
61+
62+
type TravisStatus struct {
63+
State string `json:"state"`
64+
Builds []struct {
65+
ID int64 `json:"id"`
66+
State string `json:"state"`
67+
} `json:"builds"`
68+
}
69+
70+
type TravisRequest struct {
71+
Type string `json:"@type"`
72+
Request struct {
73+
ID int64 `json:"id"`
74+
Repository struct {
75+
ID int64 `json:"id"`
76+
} `json:"repository"`
77+
} `json:"request"`
78+
}
79+
80+
var (
81+
travisTestEndpoint = "https://travis-ci.org/thefederationinfo/federation-tests/builds/%d"
82+
travisTestDescription = "Continuous integration tests for the federation network"
83+
travisTestContext = "Federation Suite"
84+
travisEndpoint = "https://api.travis-ci.org/repo/"
85+
travisSlug = "thefederationinfo%2Ffederation-tests"
86+
travisRequests = travisEndpoint + travisSlug + "/requests"
87+
)
88+
3889
func (build *Build) AfterFind(db *gorm.DB) error {
3990
return db.Model(build).Related(&build.Repo).Error
4091
}
92+
93+
func BuildAgent() {
94+
logger.Println("Started build agent")
95+
db, err := gorm.Open(databaseDriver, databaseDSN)
96+
if err != nil {
97+
panic("failed to connect database")
98+
}
99+
defer db.Close()
100+
101+
for {
102+
var builds Builds
103+
err := db.Find(&builds).Error
104+
if err != nil {
105+
logger.Printf("Cannot fetch new builds: %+v\n", err)
106+
continue
107+
}
108+
for _, build := range builds {
109+
if build.Status == BUILD_NOT_STARTED {
110+
build.Status = BUILD_PENDING
111+
err = db.Save(&build).Error
112+
if err != nil {
113+
logger.Printf("#%d: cannot update status: %+v\n", build.ID, err)
114+
continue
115+
}
116+
logger.Printf("#%d: starting new build\n", build.ID)
117+
go build.Run(false)
118+
}
119+
}
120+
time.Sleep(10 * time.Second)
121+
}
122+
logger.Println("Build agent died :S\n")
123+
}
124+
125+
func (build *Build) Run(watch bool) {
126+
db, err := gorm.Open(databaseDriver, databaseDSN)
127+
if err != nil {
128+
logger.Println(err)
129+
return
130+
}
131+
defer db.Close()
132+
133+
ts := oauth2.StaticTokenSource(
134+
&oauth2.Token{AccessToken: build.Repo.Token},
135+
)
136+
tc := oauth2.NewClient(context.Background(), ts)
137+
client := github.NewClient(tc)
138+
139+
if !watch {
140+
status := build.TriggerTravis()
141+
logger.Printf("#%d: travis build triggered\n", build.ID)
142+
build.UpdateStatus(client, status)
143+
if status == STATUS_ERROR {
144+
(*build).Status = BUILD_FINISHED
145+
err := db.Save(&build).Error
146+
if err != nil {
147+
logger.Printf("#%d: cannot update status: %+v\n", build.ID, err)
148+
}
149+
return
150+
}
151+
}
152+
153+
var statusHref string
154+
started := time.Now()
155+
timeout := started.Add(-1 * time.Hour)
156+
for {
157+
status := build.FetchStatus()
158+
logger.Printf("#%d: request status: %+v", build.ID, status)
159+
if status.State == "finished" {
160+
var failure bool
161+
var passed int
162+
for _, build := range status.Builds {
163+
// canceled, passed, errored, started
164+
switch build.State {
165+
case "canceled":
166+
fallthrough
167+
case "errored":
168+
fallthrough
169+
case "failed":
170+
failure = true
171+
case "passed":
172+
passed += 1
173+
}
174+
}
175+
if failure {
176+
build.UpdateStatus(client, STATUS_FAIL, statusHref)
177+
(*build).Status = BUILD_FINISHED
178+
err := db.Save(&build).Error
179+
if err != nil {
180+
logger.Printf("#%d: cannot update status: %+v\n", build.ID, err)
181+
}
182+
break
183+
} else if len(status.Builds) == passed {
184+
build.UpdateStatus(client, STATUS_SUCCESS, statusHref)
185+
(*build).Status = BUILD_FINISHED
186+
err := db.Save(&build).Error
187+
if err != nil {
188+
logger.Printf("#%d: cannot update status: %+v\n", build.ID, err)
189+
}
190+
break
191+
}
192+
// update the status line in the PR once
193+
if len(status.Builds) > 0 && statusHref == "" {
194+
statusHref = fmt.Sprintf(travisTestEndpoint, status.Builds[0].ID)
195+
build.UpdateStatus(client, STATUS_PENDING, statusHref)
196+
}
197+
}
198+
199+
if time.Now().Before(timeout) {
200+
build.UpdateStatus(client, STATUS_ERROR, statusHref)
201+
logger.Printf("#%d: Timeout..\n", build.ID)
202+
(*build).Status = BUILD_FINISHED
203+
err := db.Save(&build).Error
204+
if err != nil {
205+
logger.Printf("#%d: cannot update status: %+v\n", build.ID, err)
206+
}
207+
break
208+
}
209+
time.Sleep(1 * time.Minute)
210+
}
211+
logger.Printf("#%d: Travis build finished\n", build.ID)
212+
}
213+
214+
func (build *Build) TriggerTravis() string {
215+
var requestJson = `{"request":{"branch":"continuous_integration","config":{"env":{"matrix":[%s]}}}}`
216+
resp, err := build.fetch("POST", travisRequests,
217+
fmt.Sprintf(requestJson, build.Matrix))
218+
if err != nil {
219+
fmt.Println("#%d: Cannot create request: %+v", build.ID, err)
220+
return STATUS_ERROR
221+
}
222+
defer resp.Body.Close()
223+
224+
b, err := ioutil.ReadAll(resp.Body)
225+
if err != nil {
226+
fmt.Println("#%d: Cannot read status body: %+v", build.ID, err)
227+
return STATUS_ERROR
228+
}
229+
230+
var request TravisRequest
231+
err = json.Unmarshal(b, &request)
232+
if err != nil {
233+
fmt.Println("#%d: Cannot unmarshal body: %+v <> %s", build.ID, err, string(b))
234+
return STATUS_ERROR
235+
}
236+
237+
build.TravisType = request.Type
238+
build.TravisRequestID = request.Request.ID
239+
build.TravisRepositoryID = request.Request.Repository.ID
240+
241+
return STATUS_PENDING
242+
}
243+
244+
func (build *Build) UpdateStatus(client *github.Client, params... string) {
245+
if len(params) <= 0 {
246+
panic("state is mandatory")
247+
}
248+
249+
repoStatus := github.RepoStatus{
250+
State: &params[0],
251+
Description: &travisTestDescription,
252+
Context: &travisTestContext,
253+
}
254+
if len(params) >= 2 {
255+
repoStatus.TargetURL = &params[1]
256+
}
257+
if _, _, err := client.Repositories.CreateStatus(context.Background(),
258+
build.PRUser, build.PRRepo, build.PRSha, &repoStatus); err != nil {
259+
fmt.Println("#%d: Cannot update status: %+v", build.ID, err)
260+
}
261+
}
262+
263+
func (build *Build) FetchStatus() (status TravisStatus) {
264+
resp, err := build.fetch("GET", fmt.Sprintf(
265+
"%s%d%s%d", travisEndpoint, build.TravisRepositoryID,
266+
"/request/", build.TravisRequestID), "")
267+
if err != nil {
268+
logger.Printf("#%d: Cannot fetch build status: %+v\n", build.ID, err)
269+
return
270+
}
271+
defer resp.Body.Close()
272+
273+
b, err := ioutil.ReadAll(resp.Body)
274+
if err != nil {
275+
logger.Printf("#%d: Cannot read status body: %+v\n", build.ID, err)
276+
return
277+
}
278+
279+
err = json.Unmarshal(b, &status)
280+
if err != nil {
281+
logger.Printf("#%d: Cannot unmarshal body: %+v <> %s\n", build.ID, err, string(b))
282+
return
283+
}
284+
return
285+
}
286+
287+
func (build *Build) fetch(method, url, body string) (*http.Response, error) {
288+
req, err := http.NewRequest(method, url, strings.NewReader(body))
289+
if err != nil {
290+
return nil, err
291+
}
292+
req.Header.Set("Content-Type", "application/json")
293+
req.Header.Set("Accept", "application/json")
294+
req.Header.Set("Travis-API-Version", "3")
295+
req.Header.Set("Authorization", "token " + travisToken)
296+
297+
client := &http.Client{}
298+
return client.Do(req)
299+
}

server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ func main() {
8585
return
8686
}
8787

88+
// start build agent
89+
go BuildAgent()
90+
8891
http.HandleFunc("/", frontend)
8992
http.HandleFunc("/auth", authentication)
9093
http.HandleFunc("/hook", webhook)

0 commit comments

Comments
 (0)