mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-18 00:47:48 -04:00
add most tables
This commit is contained in:
parent
5a479bb034
commit
2c4f6fd42f
1
go.mod
1
go.mod
@ -233,6 +233,7 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/nektos/act v0.2.26 // indirect
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
|
289
models/bots/build.go
Normal file
289
models/bots/build.go
Normal file
@ -0,0 +1,289 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bots
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Build))
|
||||
db.RegisterModel(new(BuildIndex))
|
||||
}
|
||||
|
||||
// BuildStatus represents a build status
|
||||
type BuildStatus int
|
||||
|
||||
// enumerate all the statuses of bot build
|
||||
const (
|
||||
BuildPending BuildStatus = iota // wait for assign
|
||||
BuildAssigned // assigned to a runner
|
||||
BuildRunning // running
|
||||
BuildFailed
|
||||
BuildFinished
|
||||
BuildCanceled
|
||||
BuildTimeout
|
||||
)
|
||||
|
||||
func (status BuildStatus) IsPending() bool {
|
||||
return status == BuildPending || status == BuildAssigned
|
||||
}
|
||||
|
||||
func (status BuildStatus) IsRunning() bool {
|
||||
return status == BuildRunning
|
||||
}
|
||||
|
||||
func (status BuildStatus) IsFailed() bool {
|
||||
return status == BuildFailed || status == BuildCanceled || status == BuildTimeout
|
||||
}
|
||||
|
||||
func (status BuildStatus) IsSuccess() bool {
|
||||
return status == BuildFinished
|
||||
}
|
||||
|
||||
// Build represnets bot build task
|
||||
type Build struct {
|
||||
ID int64
|
||||
Title string
|
||||
UUID string `xorm:"CHAR(36)"`
|
||||
Index int64 `xorm:"index unique(repo_index)"`
|
||||
RepoID int64 `xorm:"index unique(repo_index)"`
|
||||
TriggerUserID int64
|
||||
TriggerUser *user_model.User `xorm:"-"`
|
||||
Ref string
|
||||
CommitSHA string
|
||||
Event webhook.HookEventType
|
||||
Token string // token for this task
|
||||
Grant string // permissions for this task
|
||||
EventPayload string `xorm:"LONGTEXT"`
|
||||
RunnerID int64 `xorm:"index"`
|
||||
Status BuildStatus `xorm:"index"`
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
StartTime timeutil.TimeStamp
|
||||
EndTime timeutil.TimeStamp
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// TableName represents a bot build
|
||||
func (Build) TableName() string {
|
||||
return "bots_build"
|
||||
}
|
||||
|
||||
func (t *Build) HTMLURL() string {
|
||||
return fmt.Sprintf("")
|
||||
}
|
||||
|
||||
func updateRepoBuildsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
||||
_, err := db.GetEngine(ctx).ID(repo.ID).
|
||||
SetExpr("num_builds",
|
||||
builder.Select("count(*)").From("bots_build").
|
||||
Where(builder.Eq{"repo_id": repo.ID}),
|
||||
).
|
||||
SetExpr("num_closed_builds",
|
||||
builder.Select("count(*)").From("bots_build").
|
||||
Where(builder.Eq{
|
||||
"repo_id": repo.ID,
|
||||
}.And(
|
||||
builder.In("status", BuildFailed, BuildCanceled, BuildTimeout, BuildFinished),
|
||||
),
|
||||
),
|
||||
).
|
||||
Update(repo)
|
||||
return err
|
||||
}
|
||||
|
||||
// InsertBuild inserts a bot build task
|
||||
func InsertBuild(t *Build, workflowsStatuses map[string]map[string]BuildStatus) error {
|
||||
if t.UUID == "" {
|
||||
t.UUID = uuid.New().String()
|
||||
}
|
||||
index, err := db.GetNextResourceIndex("bots_build_index", t.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Index = index
|
||||
|
||||
ctx, commiter, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer commiter.Close()
|
||||
|
||||
if err := db.Insert(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateRepoBuildsNumbers(ctx, &repo_model.Repository{ID: t.RepoID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buildJobs []BuildJob
|
||||
for filename, workflow := range workflowsStatuses {
|
||||
for job, status := range workflow {
|
||||
buildJobs = append(buildJobs, BuildJob{
|
||||
BuildID: t.ID,
|
||||
Filename: filename,
|
||||
Jobname: job,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := db.Insert(ctx, buildJobs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := commiter.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := CreateBuildLog(t.ID); err != nil {
|
||||
log.Error("create build log for %d table failed, will try it again when received logs", t.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateBuild updates bot build
|
||||
func UpdateBuild(t *Build, cols ...string) error {
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(t.ID).Cols(cols...).Update(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrBuildNotExist represents an error for bot build not exist
|
||||
type ErrBuildNotExist struct {
|
||||
RepoID int64
|
||||
Index int64
|
||||
UUID string
|
||||
}
|
||||
|
||||
func (err ErrBuildNotExist) Error() string {
|
||||
return fmt.Sprintf("Bot build [%s] is not exist", err.UUID)
|
||||
}
|
||||
|
||||
// GetBuildByUUID gets bot build by uuid
|
||||
func GetBuildByUUID(buildUUID string) (*Build, error) {
|
||||
var build Build
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("uuid=?", buildUUID).Get(&build)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrBuildNotExist{
|
||||
UUID: buildUUID,
|
||||
}
|
||||
}
|
||||
return &build, nil
|
||||
}
|
||||
|
||||
// GetCurBuildByID return the build for the bot
|
||||
func GetCurBuildByID(runnerID int64) (*Build, error) {
|
||||
var builds []Build
|
||||
err := db.GetEngine(db.DefaultContext).
|
||||
Where("runner_id=?", runnerID).
|
||||
And("status=?", BuildPending).
|
||||
Asc("created").
|
||||
Find(&builds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(builds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &builds[0], err
|
||||
}
|
||||
|
||||
// GetCurBuildByUUID return the task for the bot
|
||||
func GetCurBuildByUUID(runnerUUID string) (*Build, error) {
|
||||
runner, err := GetRunnerByUUID(runnerUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetCurBuildByID(runner.ID)
|
||||
}
|
||||
|
||||
func GetBuildByRepoAndIndex(repoID, index int64) (*Build, error) {
|
||||
var build Build
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("repo_id=?", repoID).
|
||||
And("`index` = ?", index).
|
||||
Get(&build)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrBuildNotExist{
|
||||
RepoID: repoID,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
return &build, nil
|
||||
}
|
||||
|
||||
// AssignBuildToRunner assign a build to a runner
|
||||
func AssignBuildToRunner(buildID int64, runnerID int64) error {
|
||||
cnt, err := db.GetEngine(db.DefaultContext).
|
||||
Where("runner_id=0").
|
||||
And("id=?", buildID).
|
||||
Cols("runner_id").
|
||||
Update(&Build{
|
||||
RunnerID: runnerID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt != 1 {
|
||||
return errors.New("assign faild")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindBuildOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
IsClosed util.OptionalBool
|
||||
}
|
||||
|
||||
func (opts FindBuildOptions) toConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.IsClosed.IsTrue() {
|
||||
cond = cond.And(builder.Expr("status IN (?,?,?,?)", BuildCanceled, BuildFailed, BuildTimeout, BuildFinished))
|
||||
} else if opts.IsClosed.IsFalse() {
|
||||
cond = cond.And(builder.Expr("status IN (?,?,?)", BuildPending, BuildAssigned, BuildRunning))
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func FindBuilds(opts FindBuildOptions) (BuildList, error) {
|
||||
sess := db.GetEngine(db.DefaultContext).Where(opts.toConds())
|
||||
if opts.ListOptions.PageSize > 0 {
|
||||
skip, take := opts.GetSkipTake()
|
||||
sess.Limit(take, skip)
|
||||
}
|
||||
var builds []*Build
|
||||
return builds, sess.Find(&builds)
|
||||
}
|
||||
|
||||
func CountBuilds(opts FindBuildOptions) (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Table("bots_build").Where(opts.toConds()).Count()
|
||||
}
|
||||
|
||||
type BuildIndex db.ResourceIndex
|
||||
|
||||
// TableName represents a bot build index
|
||||
func (BuildIndex) TableName() string {
|
||||
return "bots_build_index"
|
||||
}
|
42
models/bots/build_job.go
Normal file
42
models/bots/build_job.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bots
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
type BuildJob struct {
|
||||
ID int64
|
||||
BuildID int64 `xorm:"index"`
|
||||
Filename string
|
||||
Jobname string
|
||||
Status BuildStatus
|
||||
LogToFile bool // read log from database or from storage
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
func (bj BuildJob) TableName() string {
|
||||
return "bots_build_job"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(BuildJob))
|
||||
}
|
||||
|
||||
func GetBuildWorkflows(buildID int64) (map[string]map[string]*BuildJob, error) {
|
||||
jobs := make(map[string]map[string]*BuildJob)
|
||||
err := db.GetEngine(db.DefaultContext).Iterate(new(BuildJob), func(idx int, bean interface{}) error {
|
||||
job := bean.(*BuildJob)
|
||||
_, ok := jobs[job.Filename]
|
||||
if !ok {
|
||||
jobs[job.Filename] = make(map[string]*BuildJob)
|
||||
}
|
||||
jobs[job.Filename][job.Jobname] = job
|
||||
return nil
|
||||
})
|
||||
return jobs, err
|
||||
}
|
@ -9,13 +9,13 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
type TaskList []*Task
|
||||
type BuildList []*Build
|
||||
|
||||
// GetUserIDs returns a slice of user's id
|
||||
func (tasks TaskList) GetUserIDs() []int64 {
|
||||
func (builds BuildList) GetUserIDs() []int64 {
|
||||
userIDsMap := make(map[int64]struct{})
|
||||
for _, task := range tasks {
|
||||
userIDsMap[task.TriggerUserID] = struct{}{}
|
||||
for _, build := range builds {
|
||||
userIDsMap[build.TriggerUserID] = struct{}{}
|
||||
}
|
||||
userIDs := make([]int64, 0, len(userIDsMap))
|
||||
for userID := range userIDsMap {
|
||||
@ -24,13 +24,13 @@ func (tasks TaskList) GetUserIDs() []int64 {
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func (tasks TaskList) LoadTriggerUser() error {
|
||||
userIDs := tasks.GetUserIDs()
|
||||
func (builds BuildList) LoadTriggerUser() error {
|
||||
userIDs := builds.GetUserIDs()
|
||||
users := make(map[int64]*user_model.User, len(userIDs))
|
||||
if err := db.GetEngine(db.DefaultContext).In("id", userIDs).Find(&users); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, task := range tasks {
|
||||
for _, task := range builds {
|
||||
task.TriggerUser = users[task.TriggerUserID]
|
||||
}
|
||||
return nil
|
43
models/bots/build_log.go
Normal file
43
models/bots/build_log.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bots
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// BuildLog represents a build's log, every build has a standalone table
|
||||
type BuildLog struct {
|
||||
ID int64
|
||||
BuildJobID int64 `xorm:"index"`
|
||||
LineNumber int
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(BuildLog))
|
||||
}
|
||||
|
||||
func GetBuildLogTableName(buildID int64) string {
|
||||
return fmt.Sprintf("bots_build_log_%d", buildID)
|
||||
}
|
||||
|
||||
// CreateBuildLog table for a build
|
||||
func CreateBuildLog(buildID int64) error {
|
||||
return db.GetEngine(db.DefaultContext).
|
||||
Table(GetBuildLogTableName(buildID)).
|
||||
Sync2(new(BuildLog))
|
||||
}
|
||||
|
||||
func GetBuildLogs(buildID, jobID int64) (logs []BuildLog, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).Table(GetBuildLogTableName(buildID)).
|
||||
Where("build_job_id=?", jobID).
|
||||
Find(&logs)
|
||||
return
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bots
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Task))
|
||||
db.RegisterModel(new(BuildIndex))
|
||||
}
|
||||
|
||||
// TaskStatus represents a task status
|
||||
type TaskStatus int
|
||||
|
||||
// enumerate all the statuses of bot task
|
||||
const (
|
||||
TaskPending TaskStatus = iota // wait for assign
|
||||
TaskAssigned // assigned to a runner
|
||||
TaskRunning // running
|
||||
TaskFailed
|
||||
TaskFinished
|
||||
TaskCanceled
|
||||
TaskTimeout
|
||||
)
|
||||
|
||||
// Task represnets bot tasks
|
||||
type Task struct {
|
||||
ID int64
|
||||
Title string
|
||||
UUID string `xorm:"CHAR(36)"`
|
||||
Index int64 `xorm:"index unique(repo_index)"`
|
||||
RepoID int64 `xorm:"index unique(repo_index)"`
|
||||
TriggerUserID int64
|
||||
TriggerUser *user_model.User `xorm:"-"`
|
||||
Ref string
|
||||
CommitSHA string
|
||||
Event webhook.HookEventType
|
||||
Token string // token for this task
|
||||
Grant string // permissions for this task
|
||||
EventPayload string `xorm:"LONGTEXT"`
|
||||
RunnerID int64 `xorm:"index"`
|
||||
Status TaskStatus `xorm:"index"`
|
||||
WorkflowsStatuses map[string]map[string]TaskStatus `xorm:"LONGTEXT"`
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
StartTime timeutil.TimeStamp
|
||||
EndTime timeutil.TimeStamp
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func (t *Task) IsPending() bool {
|
||||
return t.Status == TaskPending || t.Status == TaskAssigned
|
||||
}
|
||||
|
||||
func (t *Task) IsRunning() bool {
|
||||
return t.Status == TaskRunning
|
||||
}
|
||||
|
||||
func (t *Task) IsFailed() bool {
|
||||
return t.Status == TaskFailed || t.Status == TaskCanceled || t.Status == TaskTimeout
|
||||
}
|
||||
|
||||
func (t *Task) IsSuccess() bool {
|
||||
return t.Status == TaskFinished
|
||||
}
|
||||
|
||||
// TableName represents a bot task
|
||||
func (Task) TableName() string {
|
||||
return "bots_task"
|
||||
}
|
||||
|
||||
func (t *Task) HTMLURL() string {
|
||||
return fmt.Sprintf("")
|
||||
}
|
||||
|
||||
func updateRepoBuildsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
||||
_, err := db.GetEngine(ctx).ID(repo.ID).
|
||||
SetExpr("num_builds",
|
||||
builder.Select("count(*)").From("bots_task").
|
||||
Where(builder.Eq{"repo_id": repo.ID}),
|
||||
).
|
||||
SetExpr("num_closed_builds",
|
||||
builder.Select("count(*)").From("bots_task").
|
||||
Where(builder.Eq{
|
||||
"repo_id": repo.ID,
|
||||
}.And(
|
||||
builder.In("status", TaskFailed, TaskCanceled, TaskTimeout, TaskFinished),
|
||||
),
|
||||
),
|
||||
).
|
||||
Update(repo)
|
||||
return err
|
||||
}
|
||||
|
||||
// InsertTask inserts a bot task
|
||||
func InsertTask(t *Task) error {
|
||||
if t.UUID == "" {
|
||||
t.UUID = uuid.New().String()
|
||||
}
|
||||
index, err := db.GetNextResourceIndex("build_index", t.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Index = index
|
||||
|
||||
ctx, commiter, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer commiter.Close()
|
||||
|
||||
if err := db.Insert(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateRepoBuildsNumbers(ctx, &repo_model.Repository{ID: t.RepoID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return commiter.Commit()
|
||||
}
|
||||
|
||||
// UpdateTask updates bot task
|
||||
func UpdateTask(t *Task, cols ...string) error {
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(t.ID).Cols(cols...).Update(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrTaskNotExist represents an error for bot task not exist
|
||||
type ErrTaskNotExist struct {
|
||||
RepoID int64
|
||||
Index int64
|
||||
UUID string
|
||||
}
|
||||
|
||||
func (err ErrTaskNotExist) Error() string {
|
||||
return fmt.Sprintf("Bot task [%s] is not exist", err.UUID)
|
||||
}
|
||||
|
||||
// GetTaskByUUID gets bot task by uuid
|
||||
func GetTaskByUUID(taskUUID string) (*Task, error) {
|
||||
var task Task
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("uuid=?", taskUUID).Get(&task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrTaskNotExist{
|
||||
UUID: taskUUID,
|
||||
}
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// GetCurTaskByID return the task for the bot
|
||||
func GetCurTaskByID(runnerID int64) (*Task, error) {
|
||||
var tasks []Task
|
||||
// FIXME: for test, just return all tasks
|
||||
err := db.GetEngine(db.DefaultContext).Where("status=?", TaskPending).Find(&tasks)
|
||||
// err := x.Where("runner_id = ?", botID).
|
||||
// And("status=?", BotTaskPending).
|
||||
// Find(&tasks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &tasks[0], err
|
||||
}
|
||||
|
||||
// GetCurTaskByUUID return the task for the bot
|
||||
func GetCurTaskByUUID(runnerUUID string) (*Task, error) {
|
||||
runner, err := GetRunnerByUUID(runnerUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetCurTaskByID(runner.ID)
|
||||
}
|
||||
|
||||
func GetTaskByRepoAndIndex(repoID, index int64) (*Task, error) {
|
||||
var task Task
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("repo_id=?", repoID).
|
||||
And("`index` = ?", index).
|
||||
Get(&task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrTaskNotExist{
|
||||
RepoID: repoID,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// AssignTaskToRunner assign a task to a runner
|
||||
func AssignTaskToRunner(taskID int64, runnerID int64) error {
|
||||
cnt, err := db.GetEngine(db.DefaultContext).
|
||||
Where("runner_id=0").
|
||||
And("id=?", taskID).
|
||||
Cols("runner_id").
|
||||
Update(&Task{
|
||||
RunnerID: runnerID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt != 1 {
|
||||
return errors.New("assign faild")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindTaskOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
IsClosed util.OptionalBool
|
||||
}
|
||||
|
||||
func (opts FindTaskOptions) toConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.IsClosed.IsTrue() {
|
||||
cond = cond.And(builder.Expr("status IN (?,?,?,?)", TaskCanceled, TaskFailed, TaskTimeout, TaskFinished))
|
||||
} else if opts.IsClosed.IsFalse() {
|
||||
cond = cond.And(builder.Expr("status IN (?,?,?)", TaskPending, TaskAssigned, TaskRunning))
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func FindTasks(opts FindTaskOptions) (TaskList, error) {
|
||||
sess := db.GetEngine(db.DefaultContext).Where(opts.toConds())
|
||||
if opts.ListOptions.PageSize > 0 {
|
||||
skip, take := opts.GetSkipTake()
|
||||
sess.Limit(take, skip)
|
||||
}
|
||||
var tasks []*Task
|
||||
return tasks, sess.Find(&tasks)
|
||||
}
|
||||
|
||||
func CountTasks(opts FindTaskOptions) (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Table("bots_task").Where(opts.toConds()).Count()
|
||||
}
|
||||
|
||||
type TaskStage struct{}
|
||||
|
||||
type StageStep struct{}
|
||||
|
||||
type BuildIndex db.ResourceIndex
|
@ -5,9 +5,6 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@ -29,7 +26,7 @@ func addBotTables(x *xorm.Engine) error {
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
type BotsTask struct {
|
||||
type BotsBuild struct {
|
||||
ID int64
|
||||
Title string
|
||||
UUID string `xorm:"CHAR(36)"`
|
||||
@ -55,7 +52,7 @@ func addBotTables(x *xorm.Engine) error {
|
||||
NumClosedBuilds int `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
type BuildIndex db.ResourceIndex
|
||||
type BotsBuildIndex db.ResourceIndex
|
||||
|
||||
return x.Sync2(new(BotsRunner), new(BotsTask), new(Repository), new(BuildIndex))
|
||||
return x.Sync2(new(BotsRunner), new(BotsBuild), new(Repository), new(BotsBuildIndex))
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ActionRunsUsing is the type of runner for the action
|
||||
type ActionRunsUsing string
|
||||
|
||||
func (a *ActionRunsUsing) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var using string
|
||||
if err := unmarshal(&using); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Force input to lowercase for case insensitive comparison
|
||||
format := ActionRunsUsing(strings.ToLower(using))
|
||||
switch format {
|
||||
case ActionRunsUsingNode12, ActionRunsUsingDocker:
|
||||
*a = format
|
||||
default:
|
||||
return fmt.Errorf(fmt.Sprintf("The runs.using key in action.yml must be one of: %v, got %s", []string{
|
||||
ActionRunsUsingDocker,
|
||||
ActionRunsUsingNode12,
|
||||
}, format))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// ActionRunsUsingNode12 for running with node12
|
||||
ActionRunsUsingNode12 = "node12"
|
||||
// ActionRunsUsingDocker for running with docker
|
||||
ActionRunsUsingDocker = "docker"
|
||||
)
|
||||
|
||||
// Action describes a metadata file for GitHub actions. The metadata filename must be either action.yml or action.yaml. The data in the metadata file defines the inputs, outputs and main entrypoint for your action.
|
||||
type Action struct {
|
||||
Name string `yaml:"name"`
|
||||
Author string `yaml:"author"`
|
||||
Description string `yaml:"description"`
|
||||
Inputs map[string]Input `yaml:"inputs"`
|
||||
Outputs map[string]Output `yaml:"outputs"`
|
||||
Runs struct {
|
||||
Using ActionRunsUsing `yaml:"using"`
|
||||
Env map[string]string `yaml:"env"`
|
||||
Main string `yaml:"main"`
|
||||
Image string `yaml:"image"`
|
||||
Entrypoint []string `yaml:"entrypoint"`
|
||||
Args []string `yaml:"args"`
|
||||
} `yaml:"runs"`
|
||||
Branding struct {
|
||||
Color string `yaml:"color"`
|
||||
Icon string `yaml:"icon"`
|
||||
} `yaml:"branding"`
|
||||
}
|
||||
|
||||
// Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids.
|
||||
type Input struct {
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
Default string `yaml:"default"`
|
||||
}
|
||||
|
||||
// Output parameters allow you to declare data that an action sets. Actions that run later in a workflow can use the output data set in previously run actions. For example, if you had an action that performed the addition of two inputs (x + y = z), the action could output the sum (z) for other actions to use as an input.
|
||||
type Output struct {
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// ReadAction reads an action from a reader
|
||||
func ReadAction(in io.Reader) (*Action, error) {
|
||||
a := new(Action)
|
||||
err := yaml.NewDecoder(in).Decode(a)
|
||||
return a, err
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/bot/runner"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runner.RegisterRunnerType(new(GiteaRunner))
|
||||
}
|
||||
|
||||
type GiteaRunner struct {
|
||||
}
|
||||
|
||||
func (gw *GiteaRunner) Name() string {
|
||||
return "gitea"
|
||||
}
|
||||
|
||||
func (gw *GiteaRunner) Detect(commit *git.Commit, event webhook.HookEventType, ref string) (bool, string, error) {
|
||||
tree, err := commit.SubTree(".gitea/workflow")
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
var wfs []*Workflow
|
||||
for _, entry := range entries {
|
||||
blob := entry.Blob()
|
||||
rd, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer rd.Close()
|
||||
wf, err := ReadWorkflow(rd)
|
||||
if err != nil {
|
||||
log.Error("ReadWorkflow file %s failed: %v", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
// FIXME: we have to convert the event type to github known name
|
||||
if !util.IsStringInSlice(string(event), wf.On()) {
|
||||
continue
|
||||
}
|
||||
|
||||
wfs = append(wfs, wf)
|
||||
}
|
||||
|
||||
wfBs, err := json.Marshal(wfs)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
return true, string(wfBs), nil
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// WorkflowPlanner contains methods for creating plans
|
||||
type WorkflowPlanner interface {
|
||||
PlanEvent(eventName string) *Plan
|
||||
PlanJob(jobName string) *Plan
|
||||
GetEvents() []string
|
||||
}
|
||||
|
||||
// Plan contains a list of stages to run in series
|
||||
type Plan struct {
|
||||
Stages []*Stage
|
||||
}
|
||||
|
||||
// Stage contains a list of runs to execute in parallel
|
||||
type Stage struct {
|
||||
Runs []*Run
|
||||
}
|
||||
|
||||
// Run represents a job from a workflow that needs to be run
|
||||
type Run struct {
|
||||
Workflow *Workflow
|
||||
JobID string
|
||||
}
|
||||
|
||||
func (r *Run) String() string {
|
||||
jobName := r.Job().Name
|
||||
if jobName == "" {
|
||||
jobName = r.JobID
|
||||
}
|
||||
return jobName
|
||||
}
|
||||
|
||||
// Job returns the job for this Run
|
||||
func (r *Run) Job() *Job {
|
||||
return r.Workflow.GetJob(r.JobID)
|
||||
}
|
||||
|
||||
// NewWorkflowPlanner will load a specific workflow or all workflows from a directory
|
||||
func NewWorkflowPlanner(path string) (WorkflowPlanner, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var files []os.FileInfo
|
||||
var dirname string
|
||||
|
||||
if fi.IsDir() {
|
||||
log.Debugf("Loading workflows from '%s'", path)
|
||||
dirname = path
|
||||
files, err = ioutil.ReadDir(path)
|
||||
} else {
|
||||
log.Debugf("Loading workflow '%s'", path)
|
||||
dirname, err = filepath.Abs(filepath.Dir(path))
|
||||
files = []os.FileInfo{fi}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wp := new(workflowPlanner)
|
||||
for _, file := range files {
|
||||
ext := filepath.Ext(file.Name())
|
||||
if ext == ".yml" || ext == ".yaml" {
|
||||
f, err := os.Open(filepath.Join(dirname, file.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Reading workflow '%s'", f.Name())
|
||||
workflow, err := ReadWorkflow(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
if err == io.EOF {
|
||||
return nil, errors.WithMessagef(err, "unable to read workflow, %s file is empty", file.Name())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if workflow.Name == "" {
|
||||
workflow.Name = file.Name()
|
||||
}
|
||||
wp.workflows = append(wp.workflows, workflow)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return wp, nil
|
||||
}
|
||||
|
||||
type workflowPlanner struct {
|
||||
workflows []*Workflow
|
||||
}
|
||||
|
||||
// PlanEvent builds a new list of runs to execute in parallel for an event name
|
||||
func (wp *workflowPlanner) PlanEvent(eventName string) *Plan {
|
||||
plan := new(Plan)
|
||||
if len(wp.workflows) == 0 {
|
||||
log.Debugf("no events found for workflow: %s", eventName)
|
||||
}
|
||||
|
||||
for _, w := range wp.workflows {
|
||||
for _, e := range w.When().Events {
|
||||
if e.Type == eventName {
|
||||
plan.mergeStages(createStages(w, w.GetJobIDs()...))
|
||||
}
|
||||
}
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// PlanJob builds a new run to execute in parallel for a job name
|
||||
func (wp *workflowPlanner) PlanJob(jobName string) *Plan {
|
||||
plan := new(Plan)
|
||||
if len(wp.workflows) == 0 {
|
||||
log.Debugf("no jobs found for workflow: %s", jobName)
|
||||
}
|
||||
|
||||
for _, w := range wp.workflows {
|
||||
plan.mergeStages(createStages(w, jobName))
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// GetEvents gets all the events in the workflows file
|
||||
func (wp *workflowPlanner) GetEvents() []string {
|
||||
events := make([]string, 0)
|
||||
for _, w := range wp.workflows {
|
||||
found := false
|
||||
for _, e := range events {
|
||||
for _, we := range w.When().Events {
|
||||
if e == we.Type {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
for _, evt := range w.When().Events {
|
||||
events = append(events, evt.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort the list based on depth of dependencies
|
||||
sort.Slice(events, func(i, j int) bool {
|
||||
return events[i] < events[j]
|
||||
})
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
// MaxRunNameLen determines the max name length of all jobs
|
||||
func (p *Plan) MaxRunNameLen() int {
|
||||
maxRunNameLen := 0
|
||||
for _, stage := range p.Stages {
|
||||
for _, run := range stage.Runs {
|
||||
runNameLen := len(run.String())
|
||||
if runNameLen > maxRunNameLen {
|
||||
maxRunNameLen = runNameLen
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxRunNameLen
|
||||
}
|
||||
|
||||
// GetJobIDs will get all the job names in the stage
|
||||
func (s *Stage) GetJobIDs() []string {
|
||||
names := make([]string, 0)
|
||||
for _, r := range s.Runs {
|
||||
names = append(names, r.JobID)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Merge stages with existing stages in plan
|
||||
func (p *Plan) mergeStages(stages []*Stage) {
|
||||
newStages := make([]*Stage, int(math.Max(float64(len(p.Stages)), float64(len(stages)))))
|
||||
for i := 0; i < len(newStages); i++ {
|
||||
newStages[i] = new(Stage)
|
||||
if i >= len(p.Stages) {
|
||||
newStages[i].Runs = append(newStages[i].Runs, stages[i].Runs...)
|
||||
} else if i >= len(stages) {
|
||||
newStages[i].Runs = append(newStages[i].Runs, p.Stages[i].Runs...)
|
||||
} else {
|
||||
newStages[i].Runs = append(newStages[i].Runs, p.Stages[i].Runs...)
|
||||
newStages[i].Runs = append(newStages[i].Runs, stages[i].Runs...)
|
||||
}
|
||||
}
|
||||
p.Stages = newStages
|
||||
}
|
||||
|
||||
func createStages(w *Workflow, jobIDs ...string) []*Stage {
|
||||
// first, build a list of all the necessary jobs to run, and their dependencies
|
||||
jobDependencies := make(map[string][]string)
|
||||
for len(jobIDs) > 0 {
|
||||
newJobIDs := make([]string, 0)
|
||||
for _, jID := range jobIDs {
|
||||
// make sure we haven't visited this job yet
|
||||
if _, ok := jobDependencies[jID]; !ok {
|
||||
if job := w.GetJob(jID); job != nil {
|
||||
jobDependencies[jID] = job.Needs()
|
||||
newJobIDs = append(newJobIDs, job.Needs()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
jobIDs = newJobIDs
|
||||
}
|
||||
|
||||
// next, build an execution graph
|
||||
stages := make([]*Stage, 0)
|
||||
for len(jobDependencies) > 0 {
|
||||
stage := new(Stage)
|
||||
for jID, jDeps := range jobDependencies {
|
||||
// make sure all deps are in the graph already
|
||||
if listInStages(jDeps, stages...) {
|
||||
stage.Runs = append(stage.Runs, &Run{
|
||||
Workflow: w,
|
||||
JobID: jID,
|
||||
})
|
||||
delete(jobDependencies, jID)
|
||||
}
|
||||
}
|
||||
if len(stage.Runs) == 0 {
|
||||
log.Fatalf("Unable to build dependency graph!")
|
||||
}
|
||||
stages = append(stages, stage)
|
||||
}
|
||||
|
||||
return stages
|
||||
}
|
||||
|
||||
// return true iff all strings in srcList exist in at least one of the stages
|
||||
func listInStages(srcList []string, stages ...*Stage) bool {
|
||||
for _, src := range srcList {
|
||||
found := false
|
||||
for _, stage := range stages {
|
||||
for _, search := range stage.GetJobIDs() {
|
||||
if src == search {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
@ -1,377 +0,0 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Workflow is the structure of the files in .github/workflows
|
||||
type Workflow struct {
|
||||
Name string `yaml:"name"`
|
||||
RawWhen yaml.Node `yaml:"when"`
|
||||
Env map[string]string `yaml:"env"`
|
||||
Jobs map[string]*Job `yaml:"jobs"`
|
||||
Defaults Defaults `yaml:"defaults"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
Ref string
|
||||
}
|
||||
|
||||
type When struct {
|
||||
Events []Event
|
||||
}
|
||||
|
||||
func (w *When) Match(tp string) bool {
|
||||
for _, evt := range w.Events {
|
||||
if strings.EqualFold(tp, evt.Type) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// When events for the workflow
|
||||
func (w *Workflow) When() *When {
|
||||
switch w.RawWhen.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var val string
|
||||
err := w.RawWhen.Decode(&val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return &When{
|
||||
Events: []Event{
|
||||
{
|
||||
Type: val,
|
||||
},
|
||||
},
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
var vals []string
|
||||
err := w.RawWhen.Decode(&vals)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var when When
|
||||
for _, val := range vals {
|
||||
when.Events = append(when.Events, Event{
|
||||
Type: val,
|
||||
})
|
||||
}
|
||||
return &when
|
||||
case yaml.MappingNode:
|
||||
var val map[string]interface{}
|
||||
err := w.RawWhen.Decode(&val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var keys []string
|
||||
for k := range val {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
var when When
|
||||
for _, val := range keys {
|
||||
when.Events = append(when.Events, Event{
|
||||
Type: val,
|
||||
})
|
||||
}
|
||||
return &when
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Job is the structure of one job in a workflow
|
||||
type Job struct {
|
||||
Name string `yaml:"name"`
|
||||
RawNeeds yaml.Node `yaml:"needs"`
|
||||
RawRunsOn yaml.Node `yaml:"runs-on"`
|
||||
Env map[string]string `yaml:"env"`
|
||||
If string `yaml:"if"`
|
||||
Steps []*Step `yaml:"steps"`
|
||||
TimeoutMinutes int64 `yaml:"timeout-minutes"`
|
||||
Services map[string]*ContainerSpec `yaml:"services"`
|
||||
Strategy *Strategy `yaml:"strategy"`
|
||||
RawContainer yaml.Node `yaml:"container"`
|
||||
Defaults Defaults `yaml:"defaults"`
|
||||
}
|
||||
|
||||
// Strategy for the job
|
||||
type Strategy struct {
|
||||
FailFast bool `yaml:"fail-fast"`
|
||||
MaxParallel int `yaml:"max-parallel"`
|
||||
Matrix map[string][]interface{} `yaml:"matrix"`
|
||||
}
|
||||
|
||||
// Default settings that will apply to all steps in the job or workflow
|
||||
type Defaults struct {
|
||||
Run RunDefaults `yaml:"run"`
|
||||
}
|
||||
|
||||
// Defaults for all run steps in the job or workflow
|
||||
type RunDefaults struct {
|
||||
Shell string `yaml:"shell"`
|
||||
WorkingDirectory string `yaml:"working-directory"`
|
||||
}
|
||||
|
||||
// Container details for the job
|
||||
func (j *Job) Container() *ContainerSpec {
|
||||
var val *ContainerSpec
|
||||
switch j.RawContainer.Kind {
|
||||
case yaml.ScalarNode:
|
||||
val = new(ContainerSpec)
|
||||
err := j.RawContainer.Decode(&val.Image)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case yaml.MappingNode:
|
||||
val = new(ContainerSpec)
|
||||
err := j.RawContainer.Decode(val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Needs list for Job
|
||||
func (j *Job) Needs() []string {
|
||||
|
||||
switch j.RawNeeds.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var val string
|
||||
err := j.RawNeeds.Decode(&val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return []string{val}
|
||||
case yaml.SequenceNode:
|
||||
var val []string
|
||||
err := j.RawNeeds.Decode(&val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunsOn list for Job
|
||||
func (j *Job) RunsOn() []string {
|
||||
|
||||
switch j.RawRunsOn.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var val string
|
||||
err := j.RawRunsOn.Decode(&val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return []string{val}
|
||||
case yaml.SequenceNode:
|
||||
var val []string
|
||||
err := j.RawRunsOn.Decode(&val)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMatrixes returns the matrix cross product
|
||||
func (j *Job) GetMatrixes() []map[string]interface{} {
|
||||
matrixes := make([]map[string]interface{}, 0)
|
||||
/*if j.Strategy != nil {
|
||||
includes := make([]map[string]interface{}, 0)
|
||||
for _, v := range j.Strategy.Matrix["include"] {
|
||||
includes = append(includes, v.(map[string]interface{}))
|
||||
}
|
||||
delete(j.Strategy.Matrix, "include")
|
||||
|
||||
excludes := make([]map[string]interface{}, 0)
|
||||
for _, v := range j.Strategy.Matrix["exclude"] {
|
||||
excludes = append(excludes, v.(map[string]interface{}))
|
||||
}
|
||||
delete(j.Strategy.Matrix, "exclude")
|
||||
|
||||
matrixProduct := common.CartesianProduct(j.Strategy.Matrix)
|
||||
|
||||
MATRIX:
|
||||
for _, matrix := range matrixProduct {
|
||||
for _, exclude := range excludes {
|
||||
if commonKeysMatch(matrix, exclude) {
|
||||
log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude)
|
||||
continue MATRIX
|
||||
}
|
||||
}
|
||||
matrixes = append(matrixes, matrix)
|
||||
}
|
||||
for _, include := range includes {
|
||||
log.Debugf("Adding include '%v'", include)
|
||||
matrixes = append(matrixes, include)
|
||||
}
|
||||
|
||||
} else {
|
||||
matrixes = append(matrixes, make(map[string]interface{}))
|
||||
}*/
|
||||
return matrixes
|
||||
}
|
||||
|
||||
func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
|
||||
for aKey, aVal := range a {
|
||||
if bVal, ok := b[aKey]; ok && !reflect.DeepEqual(aVal, bVal) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ContainerSpec is the specification of the container to use for the job
|
||||
type ContainerSpec struct {
|
||||
Image string `yaml:"image"`
|
||||
Env map[string]string `yaml:"env"`
|
||||
Ports []string `yaml:"ports"`
|
||||
Volumes []string `yaml:"volumes"`
|
||||
Options string `yaml:"options"`
|
||||
Entrypoint string
|
||||
Args string
|
||||
Name string
|
||||
Reuse bool
|
||||
}
|
||||
|
||||
// Step is the structure of one step in a job
|
||||
type Step struct {
|
||||
ID string `yaml:"id"`
|
||||
If string `yaml:"if"`
|
||||
Name string `yaml:"name"`
|
||||
Uses string `yaml:"uses"`
|
||||
Run string `yaml:"run"`
|
||||
WorkingDirectory string `yaml:"working-directory"`
|
||||
Shell string `yaml:"shell"`
|
||||
Env map[string]string `yaml:"env"`
|
||||
With map[string]string `yaml:"with"`
|
||||
ContinueOnError bool `yaml:"continue-on-error"`
|
||||
TimeoutMinutes int64 `yaml:"timeout-minutes"`
|
||||
}
|
||||
|
||||
// String gets the name of step
|
||||
func (s *Step) String() string {
|
||||
if s.Name != "" {
|
||||
return s.Name
|
||||
} else if s.Uses != "" {
|
||||
return s.Uses
|
||||
} else if s.Run != "" {
|
||||
return s.Run
|
||||
}
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetEnv gets the env for a step
|
||||
func (s *Step) GetEnv() map[string]string {
|
||||
rtnEnv := make(map[string]string)
|
||||
for k, v := range s.Env {
|
||||
rtnEnv[k] = v
|
||||
}
|
||||
for k, v := range s.With {
|
||||
envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(k), "_")
|
||||
envKey = fmt.Sprintf("INPUT_%s", strings.ToUpper(envKey))
|
||||
rtnEnv[envKey] = v
|
||||
}
|
||||
return rtnEnv
|
||||
}
|
||||
|
||||
// ShellCommand returns the command for the shell
|
||||
func (s *Step) ShellCommand() string {
|
||||
shellCommand := ""
|
||||
|
||||
switch s.Shell {
|
||||
case "", "bash":
|
||||
shellCommand = "bash --noprofile --norc -eo pipefail {0}"
|
||||
case "pwsh":
|
||||
shellCommand = "pwsh -command \"& '{0}'\""
|
||||
case "python":
|
||||
shellCommand = "python {0}"
|
||||
case "sh":
|
||||
shellCommand = "sh -e -c {0}"
|
||||
case "cmd":
|
||||
shellCommand = "%ComSpec% /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\""
|
||||
case "powershell":
|
||||
shellCommand = "powershell -command \"& '{0}'\""
|
||||
default:
|
||||
shellCommand = s.Shell
|
||||
}
|
||||
return shellCommand
|
||||
}
|
||||
|
||||
// StepType describes what type of step we are about to run
|
||||
type StepType int
|
||||
|
||||
const (
|
||||
// StepTypeRun is all steps that have a `run` attribute
|
||||
StepTypeRun StepType = iota
|
||||
|
||||
//StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...`
|
||||
StepTypeUsesDockerURL
|
||||
|
||||
//StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory
|
||||
StepTypeUsesActionLocal
|
||||
|
||||
//StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo
|
||||
StepTypeUsesActionRemote
|
||||
)
|
||||
|
||||
// Type returns the type of the step
|
||||
func (s *Step) Type() StepType {
|
||||
if s.Run != "" {
|
||||
return StepTypeRun
|
||||
} else if strings.HasPrefix(s.Uses, "docker://") {
|
||||
return StepTypeUsesDockerURL
|
||||
} else if strings.HasPrefix(s.Uses, "./") {
|
||||
return StepTypeUsesActionLocal
|
||||
}
|
||||
return StepTypeUsesActionRemote
|
||||
}
|
||||
|
||||
// ReadWorkflow returns a list of jobs for a given workflow file reader
|
||||
func ReadWorkflow(in io.Reader) (*Workflow, error) {
|
||||
w := new(Workflow)
|
||||
err := yaml.NewDecoder(in).Decode(w)
|
||||
return w, err
|
||||
}
|
||||
|
||||
// GetJob will get a job by name in the workflow
|
||||
func (w *Workflow) GetJob(jobID string) *Job {
|
||||
for id, j := range w.Jobs {
|
||||
if jobID == id {
|
||||
if j.Name == "" {
|
||||
j.Name = id
|
||||
}
|
||||
return j
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJobIDs will get all the job names in the workflow
|
||||
func (w *Workflow) GetJobIDs() []string {
|
||||
ids := make([]string, 0)
|
||||
for id := range w.Jobs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (w *Workflow) On() []string {
|
||||
var evts []string
|
||||
for _, job := range w.Jobs {
|
||||
evts = append(evts, job.RunsOn()...)
|
||||
}
|
||||
return evts
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReadWorkflow_StringEvent(t *testing.T) {
|
||||
yaml := `
|
||||
name: local-action-docker-url
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./actions/docker-url
|
||||
`
|
||||
|
||||
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
||||
assert.NoError(t, err, "read workflow should succeed")
|
||||
|
||||
assert.Len(t, workflow.On(), 1)
|
||||
assert.Contains(t, workflow.On(), "push")
|
||||
}
|
||||
|
||||
func TestReadWorkflow_ListEvent(t *testing.T) {
|
||||
yaml := `
|
||||
name: local-action-docker-url
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./actions/docker-url
|
||||
`
|
||||
|
||||
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
||||
assert.NoError(t, err, "read workflow should succeed")
|
||||
|
||||
assert.Len(t, workflow.On(), 2)
|
||||
assert.Contains(t, workflow.On(), "push")
|
||||
assert.Contains(t, workflow.On(), "pull_request")
|
||||
}
|
||||
|
||||
func TestReadWorkflow_MapEvent(t *testing.T) {
|
||||
yaml := `
|
||||
name: local-action-docker-url
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./actions/docker-url
|
||||
`
|
||||
|
||||
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
||||
assert.NoError(t, err, "read workflow should succeed")
|
||||
assert.Len(t, workflow.On(), 2)
|
||||
assert.Contains(t, workflow.On(), "push")
|
||||
assert.Contains(t, workflow.On(), "pull_request")
|
||||
}
|
||||
|
||||
func TestReadWorkflow_StringContainer(t *testing.T) {
|
||||
yaml := `
|
||||
name: local-action-docker-url
|
||||
|
||||
jobs:
|
||||
test:
|
||||
container: nginx:latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./actions/docker-url
|
||||
test2:
|
||||
container:
|
||||
image: nginx:latest
|
||||
env:
|
||||
foo: bar
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./actions/docker-url
|
||||
`
|
||||
|
||||
workflow, err := ReadWorkflow(strings.NewReader(yaml))
|
||||
assert.NoError(t, err, "read workflow should succeed")
|
||||
assert.Len(t, workflow.Jobs, 2)
|
||||
assert.Contains(t, workflow.Jobs["test"].Container().Image, "nginx:latest")
|
||||
assert.Contains(t, workflow.Jobs["test2"].Container().Image, "nginx:latest")
|
||||
assert.Contains(t, workflow.Jobs["test2"].Container().Env["foo"], "bar")
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
bot_model "code.gitea.io/gitea/models/bot"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/bot/runner"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
//"code.gitea.io/gitea/modules/log"
|
||||
//"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
act_runner "github.com/nektos/act/pkg/runner"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runner.RegisterRunnerType(new(GithubRunner))
|
||||
}
|
||||
|
||||
type GithubRunner struct {
|
||||
}
|
||||
|
||||
func (gw *GithubRunner) Name() string {
|
||||
return "github"
|
||||
}
|
||||
|
||||
func (gw *GithubRunner) Detect(commit *git.Commit, event webhook.HookEventType, ref string) (bool, string, error) {
|
||||
tree, err := commit.SubTree(".github/workflow")
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
var content = make(map[string]string)
|
||||
for _, entry := range entries {
|
||||
blob := entry.Blob()
|
||||
rd, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
bs, err := io.ReadAll(rd)
|
||||
rd.Close()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
content[entry.Name()] = string(bs)
|
||||
}
|
||||
|
||||
res, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
return true, string(res), nil
|
||||
}
|
||||
|
||||
func (gw *GithubRunner) Run(task *bot_model.Task) error {
|
||||
tmpDir, err := os.MkdirTemp("", fmt.Sprintf("%d", task.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var files = make(map[string]string)
|
||||
if err := json.Unmarshal([]byte(task.Content), &files); err != nil {
|
||||
return err
|
||||
}
|
||||
for name, content := range files {
|
||||
f, err := os.Create(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(task.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evtFilePath := filepath.Join(tmpDir, "event.json")
|
||||
evtFile, err := os.Create(evtFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := evtFile.WriteString(task.EventPayload); err != nil {
|
||||
evtFile.Close()
|
||||
return err
|
||||
}
|
||||
evtFile.Close()
|
||||
|
||||
planner, err := model.NewWorkflowPlanner(tmpDir, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plan := planner.PlanEvent(task.Event)
|
||||
|
||||
actor, err := user_model.GetUserByID(task.TriggerUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the plan
|
||||
config := &act_runner.Config{
|
||||
Actor: actor.LoginName,
|
||||
EventName: task.Event,
|
||||
EventPath: evtFilePath,
|
||||
DefaultBranch: repo.DefaultBranch,
|
||||
/*ForcePull: input.forcePull,
|
||||
ForceRebuild: input.forceRebuild,
|
||||
ReuseContainers: input.reuseContainers,
|
||||
Workdir: input.Workdir(),
|
||||
BindWorkdir: input.bindWorkdir,
|
||||
LogOutput: !input.noOutput,*/
|
||||
//Env: envs,
|
||||
Secrets: map[string]string{
|
||||
"token": "614e597274a527b6fcf6ddfe45def79430126f08",
|
||||
},
|
||||
//InsecureSecrets: input.insecureSecrets,*/
|
||||
Platforms: map[string]string{
|
||||
"ubuntu-latest": "node:12-buster-slim",
|
||||
"ubuntu-20.04": "node:12-buster-slim",
|
||||
"ubuntu-18.04": "node:12-buster-slim",
|
||||
},
|
||||
/*Privileged: input.privileged,
|
||||
UsernsMode: input.usernsMode,
|
||||
ContainerArchitecture: input.containerArchitecture,
|
||||
ContainerDaemonSocket: input.containerDaemonSocket,
|
||||
UseGitIgnore: input.useGitIgnore,*/
|
||||
GitHubInstance: "gitea.com",
|
||||
/*ContainerCapAdd: input.containerCapAdd,
|
||||
ContainerCapDrop: input.containerCapDrop,
|
||||
AutoRemove: input.autoRemove,
|
||||
ArtifactServerPath: input.artifactServerPath,
|
||||
ArtifactServerPort: input.artifactServerPort,*/
|
||||
}
|
||||
r, err := act_runner.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//ctx, cancel := context.WithTimeout(context.Background(), )
|
||||
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
//cancel()
|
||||
return nil
|
||||
})
|
||||
return executor(context.Background())
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
bots_model "code.gitea.io/gitea/models/bots"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
var runnerTypes = make(map[string]RunnerType)
|
||||
|
||||
type RunnerType interface {
|
||||
Name() string
|
||||
Detect(commit *git.Commit, event webhook.HookEventType, ref string) (bool, string, error)
|
||||
Run(task *bots_model.Task) error
|
||||
}
|
||||
|
||||
func RegisterRunnerType(runnerType RunnerType) {
|
||||
runnerTypes[runnerType.Name()] = runnerType
|
||||
}
|
||||
|
||||
func GetRunnerType(name string) RunnerType {
|
||||
return runnerTypes[name]
|
||||
}
|
||||
|
||||
func GetRunnerTypes() map[string]RunnerType {
|
||||
return runnerTypes
|
||||
}
|
61
modules/bots/bots.go
Normal file
61
modules/bots/bots.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bots
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
func DetectWorkflows(commit *git.Commit, event webhook.HookEventType) (git.Entries, []map[string]*model.Job, error) {
|
||||
tree, err := commit.SubTree(".github/workflows")
|
||||
if _, ok := err.(git.ErrNotExist); ok {
|
||||
tree, err = commit.SubTree(".gitea/workflows")
|
||||
}
|
||||
if _, ok := err.(git.ErrNotExist); ok {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
entries, err := tree.ListEntriesRecursive()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
matchedEntries := make(git.Entries, 0, len(entries))
|
||||
jobs := make([]map[string]*model.Job, 0, len(entries))
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".yml") && !strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
continue
|
||||
}
|
||||
f, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
workflow, err := model.ReadWorkflow(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, e := range workflow.On() {
|
||||
if e == event.Event() {
|
||||
matchedEntries = append(matchedEntries, entry)
|
||||
jobs = append(jobs, workflow.Jobs)
|
||||
break
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
return matchedEntries, jobs, nil
|
||||
}
|
@ -16,6 +16,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
bots_module "code.gitea.io/gitea/modules/bots"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
@ -39,28 +40,6 @@ func NewNotifier() base.Notifier {
|
||||
return &botsNotifier{}
|
||||
}
|
||||
|
||||
func detectWorkflows(commit *git.Commit, event webhook.HookEventType, ref string) (bool, error) {
|
||||
tree, err := commit.SubTree(".github/workflows")
|
||||
if _, ok := err.(git.ErrNotExist); ok {
|
||||
tree, err = commit.SubTree(".gitea/workflows")
|
||||
}
|
||||
if _, ok := err.(git.ErrNotExist); ok {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Trace("detected %s has %d entries", commit.ID, len(entries))
|
||||
|
||||
return len(entries) > 0, nil
|
||||
}
|
||||
|
||||
func notifyIssue(issue *models.Issue, doer *user_model.User, evt webhook.HookEventType, payload string) {
|
||||
err := issue.LoadRepo(db.DefaultContext)
|
||||
if err != nil {
|
||||
@ -75,8 +54,11 @@ func notifyIssue(issue *models.Issue, doer *user_model.User, evt webhook.HookEve
|
||||
if ref == "" {
|
||||
ref = issue.Repo.DefaultBranch
|
||||
}
|
||||
notify(issue.Repo, doer, payload, ref, evt)
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(context.Background(), issue.Repo.RepoPath())
|
||||
func notify(repo *repo_model.Repository, doer *user_model.User, payload, ref string, evt webhook.HookEventType) {
|
||||
gitRepo, err := git.OpenRepository(context.Background(), repo.RepoPath())
|
||||
if err != nil {
|
||||
log.Error("issue.LoadRepo: %v", err)
|
||||
return
|
||||
@ -90,31 +72,41 @@ func notifyIssue(issue *models.Issue, doer *user_model.User, evt webhook.HookEve
|
||||
return
|
||||
}
|
||||
|
||||
hasWorkflows, err := detectWorkflows(commit, evt, ref)
|
||||
matchedEntries, jobs, err := bots_module.DetectWorkflows(commit, evt)
|
||||
if err != nil {
|
||||
log.Error("detectWorkflows: %v", err)
|
||||
return
|
||||
}
|
||||
if !hasWorkflows {
|
||||
log.Trace("repo %s with commit %s couldn't find workflows", issue.Repo.RepoPath(), commit.ID)
|
||||
log.Trace("detected %s has %d entries", commit.ID, len(matchedEntries))
|
||||
if len(matchedEntries) == 0 {
|
||||
log.Trace("repo %s with commit %s couldn't find workflows", repo.RepoPath(), commit.ID)
|
||||
return
|
||||
}
|
||||
|
||||
task := bots_model.Task{
|
||||
Title: commit.CommitMessage,
|
||||
RepoID: issue.RepoID,
|
||||
workflowsStatuses := make(map[string]map[string]bots_model.BuildStatus)
|
||||
for i, entry := range matchedEntries {
|
||||
taskStatuses := make(map[string]bots_model.BuildStatus)
|
||||
for k := range jobs[i] {
|
||||
taskStatuses[k] = bots_model.BuildPending
|
||||
}
|
||||
workflowsStatuses[entry.Name()] = taskStatuses
|
||||
}
|
||||
|
||||
build := bots_model.Build{
|
||||
Title: commit.Message(),
|
||||
RepoID: repo.ID,
|
||||
TriggerUserID: doer.ID,
|
||||
Event: evt,
|
||||
EventPayload: payload,
|
||||
Status: bots_model.TaskPending,
|
||||
Status: bots_model.BuildPending,
|
||||
Ref: ref,
|
||||
CommitSHA: commit.ID.String(),
|
||||
}
|
||||
|
||||
if err := bots_model.InsertTask(&task); err != nil {
|
||||
if err := bots_model.InsertBuild(&build, workflowsStatuses); err != nil {
|
||||
log.Error("InsertBotTask: %v", err)
|
||||
} else {
|
||||
bots_service.PushToQueue(&task)
|
||||
bots_service.PushToQueue(&build)
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,29 +182,6 @@ func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_mod
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||
if err != nil {
|
||||
log.Error("commits.ToAPIPayloadCommits failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetCommit(commits.HeadCommit.Sha1)
|
||||
if err != nil {
|
||||
log.Error("commits.ToAPIPayloadCommits failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
hasWorkflows, err := detectWorkflows(commit, webhook.HookEventPush, opts.RefFullName)
|
||||
if err != nil {
|
||||
log.Error("detectWorkflows: %v", err)
|
||||
return
|
||||
}
|
||||
if !hasWorkflows {
|
||||
log.Trace("repo %s with commit %s couldn't find workflows", repo.RepoPath(), commit.ID)
|
||||
return
|
||||
}
|
||||
|
||||
payload := &api.PushPayload{
|
||||
Ref: opts.RefFullName,
|
||||
Before: opts.OldCommitID,
|
||||
@ -231,20 +200,7 @@ func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_mod
|
||||
return
|
||||
}
|
||||
|
||||
task := bots_model.Task{
|
||||
Title: commit.Message(),
|
||||
RepoID: repo.ID,
|
||||
TriggerUserID: pusher.ID,
|
||||
Event: webhook.HookEventPush,
|
||||
EventPayload: string(bs),
|
||||
Status: bots_model.TaskPending,
|
||||
}
|
||||
|
||||
if err := bots_model.InsertTask(&task); err != nil {
|
||||
log.Error("InsertBotTask: %v", err)
|
||||
} else {
|
||||
bots_service.PushToQueue(&task)
|
||||
}
|
||||
notify(repo, pusher, string(bs), opts.RefFullName, webhook.HookEventPush)
|
||||
}
|
||||
|
||||
func (a *botsNotifier) NotifyCreateRef(doer *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) {
|
||||
|
@ -147,7 +147,7 @@ MESSAGE_BUMP:
|
||||
}
|
||||
|
||||
// TODO: find new task and send to client
|
||||
task, err := bots_model.GetCurTaskByUUID(msg.RunnerUUID)
|
||||
task, err := bots_model.GetCurBuildByUUID(msg.RunnerUUID)
|
||||
if err != nil {
|
||||
log.Error("websocket[%s] get task failed: %v", r.RemoteAddr, err)
|
||||
break
|
||||
|
@ -5,6 +5,7 @@
|
||||
package builds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
bots_model "code.gitea.io/gitea/models/bots"
|
||||
@ -45,7 +46,7 @@ func List(ctx *context.Context) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
opts := bots_model.FindTaskOptions{
|
||||
opts := bots_model.FindBuildOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
@ -57,24 +58,24 @@ func List(ctx *context.Context) {
|
||||
} else {
|
||||
opts.IsClosed = util.OptionalBoolFalse
|
||||
}
|
||||
tasks, err := bots_model.FindTasks(opts)
|
||||
builds, err := bots_model.FindBuilds(opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := tasks.LoadTriggerUser(); err != nil {
|
||||
if err := builds.LoadTriggerUser(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
total, err := bots_model.CountTasks(opts)
|
||||
total, err := bots_model.CountBuilds(opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Tasks"] = tasks
|
||||
ctx.Data["Builds"] = builds
|
||||
|
||||
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
@ -85,15 +86,64 @@ func List(ctx *context.Context) {
|
||||
|
||||
func ViewBuild(ctx *context.Context) {
|
||||
index := ctx.ParamsInt64("index")
|
||||
task, err := bots_model.GetTaskByRepoAndIndex(ctx.Repo.Repository.ID, index)
|
||||
build, err := bots_model.GetBuildByRepoAndIndex(ctx.Repo.Repository.ID, index)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = task.Title + " - " + ctx.Tr("repo.builds")
|
||||
ctx.Data["Title"] = build.Title + " - " + ctx.Tr("repo.builds")
|
||||
ctx.Data["PageIsBuildList"] = true
|
||||
ctx.Data["Build"] = task
|
||||
ctx.Data["Build"] = build
|
||||
statuses, err := bots_model.GetBuildWorkflows(build.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["WorkflowsStatuses"] = statuses
|
||||
|
||||
ctx.HTML(http.StatusOK, tplViewBuild)
|
||||
}
|
||||
|
||||
func GetBuildJobLogs(ctx *context.Context) {
|
||||
index := ctx.ParamsInt64("index")
|
||||
build, err := bots_model.GetBuildByRepoAndIndex(ctx.Repo.Repository.ID, index)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
workflows, err := bots_model.GetBuildWorkflows(build.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
var buildJob *bots_model.BuildJob
|
||||
wf := ctx.Params("workflow")
|
||||
jobname := ctx.Params("jobname")
|
||||
LOOP_WORKFLOWS:
|
||||
for workflow, jobs := range workflows {
|
||||
if workflow == wf {
|
||||
for _, job := range jobs {
|
||||
if jobname == job.Jobname {
|
||||
buildJob = job
|
||||
break LOOP_WORKFLOWS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if buildJob == nil {
|
||||
ctx.Error(http.StatusNotFound, fmt.Sprintf("workflow %s job %s not exist", wf, jobname))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: if buildJob.LogToFile is true, read the logs from the file
|
||||
|
||||
logs, err := bots_model.GetBuildLogs(build.ID, buildJob.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, logs)
|
||||
}
|
||||
|
@ -1175,6 +1175,7 @@ func RegisterRoutes(m *web.Route) {
|
||||
m.Get("", builds.List)
|
||||
m.Group("/{index}", func() {
|
||||
m.Get("", builds.ViewBuild)
|
||||
m.Get("/{workflow}/job/{jobname}/logs", builds.GetBuildJobLogs)
|
||||
})
|
||||
}, reqRepoBuildsReader, builds.MustEnableBuilds)
|
||||
|
||||
|
@ -14,16 +14,16 @@ import (
|
||||
//"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
// taskQueue is a global queue of tasks
|
||||
var taskQueue queue.Queue
|
||||
// buildQueue is a global queue of bot build
|
||||
var buildQueue queue.Queue
|
||||
|
||||
// PushToQueue
|
||||
func PushToQueue(task *bots_model.Task) {
|
||||
taskQueue.Push(task)
|
||||
func PushToQueue(task *bots_model.Build) {
|
||||
buildQueue.Push(task)
|
||||
}
|
||||
|
||||
// Dispatch assign a task to a runner
|
||||
func Dispatch(task *bots_model.Task) (*bots_model.Runner, error) {
|
||||
func Dispatch(task *bots_model.Build) (*bots_model.Runner, error) {
|
||||
runner, err := bots_model.GetUsableRunner(bots_model.GetRunnerOptions{
|
||||
RepoID: task.RepoID,
|
||||
})
|
||||
@ -31,17 +31,17 @@ func Dispatch(task *bots_model.Task) (*bots_model.Runner, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return runner, bots_model.AssignTaskToRunner(task.ID, runner.ID)
|
||||
return runner, bots_model.AssignBuildToRunner(task.ID, runner.ID)
|
||||
}
|
||||
|
||||
// Init will start the service to get all unfinished tasks and run them
|
||||
func Init() error {
|
||||
taskQueue = queue.CreateQueue("actions_task", handle, &bots_model.Task{})
|
||||
if taskQueue == nil {
|
||||
buildQueue = queue.CreateQueue("actions_task", handle, &bots_model.Build{})
|
||||
if buildQueue == nil {
|
||||
return fmt.Errorf("Unable to create Task Queue")
|
||||
}
|
||||
|
||||
go graceful.GetManager().RunWithShutdownFns(taskQueue.Run)
|
||||
go graceful.GetManager().RunWithShutdownFns(buildQueue.Run)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -49,13 +49,13 @@ func Init() error {
|
||||
func handle(data ...queue.Data) []queue.Data {
|
||||
var unhandled []queue.Data
|
||||
for _, datum := range data {
|
||||
task := datum.(*bots_model.Task)
|
||||
runner, err := Dispatch(task)
|
||||
build := datum.(*bots_model.Build)
|
||||
runner, err := Dispatch(build)
|
||||
if err != nil {
|
||||
log.Error("Run task failed: %v", err)
|
||||
unhandled = append(unhandled, task)
|
||||
log.Error("Run build failed: %v", err)
|
||||
unhandled = append(unhandled, build)
|
||||
} else {
|
||||
log.Trace("task %v assigned to %s", task.UUID, runner.UUID)
|
||||
log.Trace("build %v assigned to %s", build.UUID, runner.UUID)
|
||||
}
|
||||
}
|
||||
return unhandled
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="issue list">
|
||||
{{range .Tasks}}
|
||||
{{range .Builds}}
|
||||
<li class="item df py-3">
|
||||
<div class="issue-item-left df">
|
||||
{{if $.CanWriteIssuesOrPulls}}
|
||||
@ -8,20 +8,7 @@
|
||||
<label></label>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="issue-item-icon">
|
||||
{{if .IsPending}}
|
||||
<i class="commit-status circle icon gray"></i>
|
||||
{{end}}
|
||||
{{if .IsRunning}}
|
||||
<i class="commit-status circle icon yellow"></i>
|
||||
{{end}}
|
||||
{{if .IsSuccess}}
|
||||
<i class="commit-status check icon green"></i>
|
||||
{{end}}
|
||||
{{if .IsFailed}}
|
||||
<i class="commit-status warning icon red"></i>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "repo/builds/status" .Status}}
|
||||
</div>
|
||||
<div class="issue-item-main f1 fc df">
|
||||
<div class="desc issue-item-bottom-row df ac fw my-1">
|
||||
|
14
templates/repo/builds/status.tmpl
Normal file
14
templates/repo/builds/status.tmpl
Normal file
@ -0,0 +1,14 @@
|
||||
<div class="issue-item-icon">
|
||||
{{if .IsPending}}
|
||||
<i class="commit-status circle icon gray"></i>
|
||||
{{end}}
|
||||
{{if .IsRunning}}
|
||||
<i class="commit-status circle icon yellow"></i>
|
||||
{{end}}
|
||||
{{if .IsSuccess}}
|
||||
<i class="commit-status check icon green"></i>
|
||||
{{end}}
|
||||
{{if .IsFailed}}
|
||||
<i class="commit-status warning icon red"></i>
|
||||
{{end}}
|
||||
</div>
|
@ -10,6 +10,7 @@
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{template "repo/builds/view_left" .}}
|
||||
{{template "repo/builds/view_content" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,19 @@
|
||||
<div class="ui stackable grid">
|
||||
{{if .Flash}}
|
||||
<div class="sixteen wide column">
|
||||
{{template "base/alert" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="ui stackable grid" id="build_log">
|
||||
<div class="console_wrapper__3ow2p" style="height: 206px;">
|
||||
<header class="console_header__4K9_D">
|
||||
<div class="console_header-inner__29khj">
|
||||
<div class="console_info__1NL_l">
|
||||
<h3>Console Logs</h3>
|
||||
</div>
|
||||
<div class="console_controls__QeCq_">
|
||||
<button class="button button_theme-plain__2mkOw button_plain__1LweR size-md" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button></div></div></header><pre class="console_terminal__3DK3w"><code class="ansi-hook console_output__2qZpe">
|
||||
<div class="console_line__1ir27"><span class="console_line-number__3zolU">1</span><span class="loc-html console_line-content__3xTWR">Initialized empty Git repository in /drone/src/.git/
|
||||
</span><span class="console_line-time__oQvWj">0s</span></div><div class="console_line__1ir27"><span class="console_line-number__3zolU">2</span><span class="loc-html console_line-content__3xTWR">+ git fetch origin +refs/heads/main:
|
||||
</span><span class="console_line-time__oQvWj">0s</span></div><div class="console_line__1ir27"><span class="console_line-number__3zolU">3</span><span class="loc-html console_line-content__3xTWR">From https://github.com/go-gitea/gitea
|
||||
</span><span class="console_line-time__oQvWj">20s</span></div><div class="console_line__1ir27"><span class="console_line-number__3zolU">4</span><span class="loc-html console_line-content__3xTWR"> * branch main -> FETCH_HEAD
|
||||
</span><span class="console_line-time__oQvWj">20s</span></div><div class="console_line__1ir27"><span class="console_line-number__3zolU">5</span><span class="loc-html console_line-content__3xTWR"> * [new branch] main -> origin/main
|
||||
</span><span class="console_line-time__oQvWj">20s</span></div><div class="console_line__1ir27"><span class="console_line-number__3zolU">6</span><span class="loc-html console_line-content__3xTWR">+ git checkout c8ec2261a99590f15699e9147a28e4b61c1c2ea5 -b main
|
||||
</span><span class="console_line-time__oQvWj">20s</span></div><div class="console_line__1ir27"><span class="console_line-number__3zolU">7</span><span class="loc-html console_line-content__3xTWR">Switched to a new branch 'main'
|
||||
</span><span class="console_line-time__oQvWj">20s</span></div><div></div></code></pre><footer class="console_footer__3xmc8"><div class="console_summary__1762k"><div class="console_summary-info__3mKSP"><div class="status_status__1f9yu status_status-success__2WE4F console_summary-status__2Vetb" title="Status: success"><svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path fill="none" d="M0 0h20v20H0z"></path><path d="M14.577 6.23a.887.887 0 0 1 1.17-.019.704.704 0 0 1 .021 1.063l-6.844 6.439-.025.023a1.11 1.11 0 0 1-1.463-.023l-3.204-3.015a.704.704 0 0 1 .021-1.063.887.887 0 0 1 1.17.019l2.757 2.594 6.397-6.018z" fill="currentColor" fill-rule="nonzero"></path></svg></div>Exit Code 0</div><div class="console_summary-controls__Siy-a"></div></div></footer></div>
|
||||
</div>
|
||||
|
18
templates/repo/builds/view_left.tmpl
Normal file
18
templates/repo/builds/view_left.tmpl
Normal file
@ -0,0 +1,18 @@
|
||||
<div class="ui dividing left rail">
|
||||
<div class="ui sticky fixed top" style="width: 283px !important; height: 1002.03px !important; margin-top: 30px; left: 1101px; top: 0px;">
|
||||
<h4 class="ui header">Menu</h4>
|
||||
<div class="ui vertical following fluid accordion text menu">
|
||||
{{range $file, $jobs := .WorkflowsStatuses}}
|
||||
<div class="item">
|
||||
<a class="active title"><i class="dropdown icon"></i> <b>{{ $file }}</b></a>
|
||||
<div class="active content menu">
|
||||
{{range $jobname, $job := $jobs}}
|
||||
{{template "repo/builds/status" $job.Status}}
|
||||
<a class="item" href="#{{$file}}__{{$jobname}}">{{ $jobname }}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user