Compare commits

...

3 Commits

Author SHA1 Message Date
4f2c2dc8da
Order tasks by change date first
All checks were successful
Alpine 3.20 Success
The user presumably does not want to look everywhere for recent tasks.
2024-12-26 16:24:54 +01:00
55a6693942
Fix deployment error processing 2024-12-26 16:18:37 +01:00
d5981249b1
Add time information 2024-12-26 16:17:45 +01:00
2 changed files with 142 additions and 19 deletions

View File

@ -67,6 +67,17 @@ which has the following fields:
*RunnerName*::
Descriptive name of the runner.
// Intentionally not documenting CreatedUnix, ChangedUnix, DurationSeconds,
// which can be derived from the objects.
*Created*, *Changed*::
`*time.Time` of task creation and last task state change respectively,
or nil if not known.
*CreatedAgo*, *ChangedAgo*::
Abbreviated human-friendly relative elapsed time duration
since *Created* and *Changed* respectively.
*Duration*::
`*time.Duration` of the last run in seconds, or nil if not known.
*URL*::
*acid* link to the task, where its log output can be seen.
*RepoURL*::

150
acid.go
View File

@ -30,6 +30,7 @@ import (
"syscall"
ttemplate "text/template"
"time"
"unicode"
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/sftp"
@ -173,6 +174,7 @@ func giteaNewRequest(ctx context.Context, method, path string, body io.Reader) (
func getTasks(ctx context.Context, query string, args ...any) ([]Task, error) {
rows, err := gDB.QueryContext(ctx, `
SELECT id, owner, repo, hash, runner,
created, changed, duration,
state, detail, notified,
runlog, tasklog, deploylog FROM task `+query, args...)
if err != nil {
@ -184,11 +186,13 @@ func getTasks(ctx context.Context, query string, args ...any) ([]Task, error) {
for rows.Next() {
var t Task
err := rows.Scan(&t.ID, &t.Owner, &t.Repo, &t.Hash, &t.Runner,
&t.CreatedUnix, &t.ChangedUnix, &t.DurationSeconds,
&t.State, &t.Detail, &t.Notified,
&t.RunLog, &t.TaskLog, &t.DeployLog)
if err != nil {
return nil, err
}
// We could also update some fields from gRunning.
tasks = append(tasks, t)
}
return tasks, rows.Err()
@ -209,6 +213,8 @@ var templateTasks = template.Must(template.New("tasks").Parse(`
<thead>
<tr>
<th>ID</th>
<th>Created</th>
<th>Changed</th>
<th>Repository</th>
<th>Hash</th>
<th>Runner</th>
@ -221,6 +227,8 @@ var templateTasks = template.Must(template.New("tasks").Parse(`
{{range .}}
<tr>
<td><a href="task/{{.ID}}">{{.ID}}</a></td>
<td align="right"><span title="{{.Created}}">{{.CreatedAgo}}</span></td>
<td align="right"><span title="{{.Changed}}">{{.ChangedAgo}}</span></td>
<td><a href="{{.RepoURL}}">{{.FullName}}</a></td>
<td><a href="{{.CommitURL}}">{{.Hash}}</a></td>
<td>{{.RunnerName}}</td>
@ -236,7 +244,7 @@ var templateTasks = template.Must(template.New("tasks").Parse(`
`))
func handleTasks(w http.ResponseWriter, r *http.Request) {
tasks, err := getTasks(r.Context(), `ORDER BY id DESC`)
tasks, err := getTasks(r.Context(), `ORDER BY changed DESC, id DESC`)
if err != nil {
http.Error(w,
"Error retrieving tasks: "+err.Error(),
@ -262,6 +270,14 @@ var templateTask = template.Must(template.New("tasks").Parse(`
<body>
<h1><a href="..">Tasks</a> &raquo; {{.ID}}</h1>
<dl>
{{if .Created}}
<dt>Created</dt>
<dd><span title="{{.Created}}">{{.CreatedAgo}} ago</span></dd>
{{end}}
{{if .Changed}}
<dt>Changed</dt>
<dd><span title="{{.Changed}}">{{.ChangedAgo}} ago</span></dd>
{{end}}
<dt>Project</dt>
<dd><a href="{{.RepoURL}}">{{.FullName}}</a></dd>
<dt>Commit</dt>
@ -272,6 +288,10 @@ var templateTask = template.Must(template.New("tasks").Parse(`
<dd>{{.State}}{{if .Detail}} ({{.Detail}}){{end}}</dd>
<dt>Notified</dt>
<dd>{{.Notified}}</dd>
{{if .Duration}}
<dt>Duration</dt>
<dd>{{.Duration}}</dd>
{{end}}
</dl>
{{if .RunLog}}
<h2>Runner log</h2>
@ -324,6 +344,8 @@ func handleTask(w http.ResponseWriter, r *http.Request) {
return
}
task.DurationSeconds = rt.elapsed()
rt.RunLog.Lock()
defer rt.RunLog.Unlock()
rt.TaskLog.Lock()
@ -364,8 +386,9 @@ func createTasks(ctx context.Context,
}
defer tx.Rollback()
stmt, err := tx.Prepare(`INSERT INTO task(owner, repo, hash, runner)
VALUES (?, ?, ?, ?)`)
stmt, err := tx.Prepare(
`INSERT INTO task(owner, repo, hash, runner, created, changed)
VALUES (?, ?, ?, ?, unixepoch('now'), unixepoch('now'))`)
if err != nil {
return err
}
@ -446,8 +469,11 @@ func rpcRestartOne(ctx context.Context, id int64) error {
// The executor bumps to "running" after inserting into gRunning,
// so we should not need to exclude that state here.
result, err := gDB.ExecContext(ctx, `UPDATE task
SET state = ?, detail = '', notified = 0 WHERE id = ?`,
//
// We deliberately do not clear previous run data (duration, *log).
result, err := gDB.ExecContext(ctx,
`UPDATE task SET changed = unixepoch('now'),
state = ?, detail = '', notified = 0 WHERE id = ?`,
taskStateNew, id)
if err != nil {
return fmt.Errorf("%d: %w", id, err)
@ -840,6 +866,9 @@ func newRunningTask(task Task) (*RunningTask, error) {
rt := &RunningTask{DB: task}
config := getConfig()
// This is for our own tracking, not actually written to database.
rt.DB.ChangedUnix = time.Now().Unix()
var ok bool
rt.Runner, ok = config.Runners[rt.DB.Runner]
if !ok {
@ -935,6 +964,10 @@ func (rt *RunningTask) localEnv() []string {
)
}
func (rt *RunningTask) elapsed() int64 {
return int64(time.Since(time.Unix(rt.DB.ChangedUnix, 0)).Seconds())
}
// update stores the running task's state in the database.
func (rt *RunningTask) update() error {
for _, i := range []struct {
@ -951,6 +984,7 @@ func (rt *RunningTask) update() error {
*i.log = []byte{}
}
}
rt.DB.DurationSeconds = rt.elapsed()
return rt.DB.update()
}
@ -1175,6 +1209,7 @@ func executorConnect(
func executorRunTask(ctx context.Context, task Task) error {
rt, err := newRunningTask(task)
if err != nil {
task.DurationSeconds = 0
task.State, task.Detail = taskStateError, "Misconfigured"
task.Notified = 0
task.RunLog = []byte(err.Error())
@ -1194,6 +1229,7 @@ func executorRunTask(ctx context.Context, task Task) error {
f()
}
locked(func() {
rt.DB.DurationSeconds = 0
rt.DB.State, rt.DB.Detail = taskStateRunning, ""
rt.DB.Notified = 0
rt.DB.RunLog = []byte{}
@ -1281,19 +1317,20 @@ func executorRunTask(ctx context.Context, task Task) error {
defer client.Close()
var (
ee1 *ssh.ExitError
ee2 *executorError
eeSSH *ssh.ExitError
eeExec *exec.ExitError
ee3 *executorError
)
err = executorBuild(ctxRunner, client, rt)
if err != nil {
locked(func() {
if errors.As(err, &ee1) {
if errors.As(err, &eeSSH) {
rt.DB.State, rt.DB.Detail = taskStateFailed, "Scripts failed"
fmt.Fprintf(&rt.TaskLog, "\n%s\n", err)
} else if errors.As(err, &ee2) {
rt.DB.State, rt.DB.Detail = taskStateError, ee2.Detail
fmt.Fprintf(&rt.TaskLog, "\n%s\n", ee2.Err)
} else if errors.As(err, &ee3) {
rt.DB.State, rt.DB.Detail = taskStateError, ee3.Detail
fmt.Fprintf(&rt.TaskLog, "\n%s\n", ee3.Err)
} else {
rt.DB.State, rt.DB.Detail = taskStateError, ""
fmt.Fprintf(&rt.TaskLog, "\n%s\n", err)
@ -1313,12 +1350,12 @@ func executorRunTask(ctx context.Context, task Task) error {
locked(func() {
if err == nil {
rt.DB.State, rt.DB.Detail = taskStateSuccess, ""
} else if errors.As(err, &ee1) {
} else if errors.As(err, &eeExec) {
rt.DB.State, rt.DB.Detail = taskStateFailed, "Deployment failed"
fmt.Fprintf(&rt.DeployLog, "\n%s\n", err)
} else if errors.As(err, &ee2) {
rt.DB.State, rt.DB.Detail = taskStateError, ee2.Detail
fmt.Fprintf(&rt.DeployLog, "\n%s\n", ee2.Err)
} else if errors.As(err, &ee3) {
rt.DB.State, rt.DB.Detail = taskStateError, ee3.Detail
fmt.Fprintf(&rt.DeployLog, "\n%s\n", ee3.Err)
} else {
rt.DB.State, rt.DB.Detail = taskStateError, ""
fmt.Fprintf(&rt.DeployLog, "\n%s\n", err)
@ -1402,6 +1439,11 @@ type Task struct {
Hash string
Runner string
// True database names for these are occupied by accessors.
CreatedUnix int64
ChangedUnix int64
DurationSeconds int64
State taskState
Detail string
Notified int64
@ -1437,10 +1479,64 @@ func (t *Task) CloneURL() string {
return fmt.Sprintf("%s/%s/%s.git", getConfig().Gitea, t.Owner, t.Repo)
}
func shortDurationString(d time.Duration) string {
rs := []rune(d.Truncate(time.Second).String())
for i, r := range rs {
if !unicode.IsLetter(r) {
continue
}
i++
for i < len(rs) && unicode.IsLetter(rs[i]) {
i++
}
return string(rs[:i])
}
return string(rs)
}
func (t *Task) Created() *time.Time {
if t.CreatedUnix == 0 {
return nil
}
tt := time.Unix(t.CreatedUnix, 0)
return &tt
}
func (t *Task) Changed() *time.Time {
if t.ChangedUnix == 0 {
return nil
}
tt := time.Unix(t.ChangedUnix, 0)
return &tt
}
func (t *Task) CreatedAgo() string {
if t.CreatedUnix == 0 {
return ""
}
return shortDurationString(time.Since(*t.Created()))
}
func (t *Task) ChangedAgo() string {
if t.ChangedUnix == 0 {
return ""
}
return shortDurationString(time.Since(*t.Changed()))
}
func (t *Task) Duration() *time.Duration {
if t.DurationSeconds == 0 {
return nil
}
td := time.Duration(t.DurationSeconds * int64(time.Second))
return &td
}
func (t *Task) update() error {
_, err := gDB.ExecContext(context.Background(), `UPDATE task
SET state = ?, detail = ?, notified = ?,
_, err := gDB.ExecContext(context.Background(),
`UPDATE task SET changed = unixepoch('now'), duration = ?,
state = ?, detail = ?, notified = ?,
runlog = ?, tasklog = ?, deploylog = ? WHERE id = ?`,
t.DurationSeconds,
t.State, t.Detail, t.Notified,
t.RunLog, t.TaskLog, t.DeployLog, t.ID)
if err == nil {
@ -1462,6 +1558,10 @@ CREATE TABLE IF NOT EXISTS task(
hash TEXT NOT NULL, -- commit hash
runner TEXT NOT NULL, -- the runner to use
created INTEGER NOT NULL DEFAULT 0, -- creation timestamp
changed INTEGER NOT NULL DEFAULT 0, -- last state change timestamp
duration INTEGER NOT NULL DEFAULT 0, -- duration of last run
state INTEGER NOT NULL DEFAULT 0, -- task state
detail TEXT NOT NULL DEFAULT '', -- task state detail
notified INTEGER NOT NULL DEFAULT 0, -- Gitea knows the state
@ -1519,13 +1619,25 @@ func dbOpen(path string) error {
`task`, `deploylog`, `BLOB NOT NULL DEFAULT x''`); err != nil {
return err
}
break
case 1:
if err = dbEnsureColumn(tx,
`task`, `created`, `INTEGER NOT NULL DEFAULT 0`); err != nil {
return err
}
if err = dbEnsureColumn(tx,
`task`, `changed`, `INTEGER NOT NULL DEFAULT 0`); err != nil {
return err
}
if err = dbEnsureColumn(tx,
`task`, `duration`, `INTEGER NOT NULL DEFAULT 0`); err != nil {
return err
}
case 2:
// The next migration goes here, remember to increment the number below.
}
if _, err = tx.Exec(
`PRAGMA user_version = ` + strconv.Itoa(1)); err != nil {
`PRAGMA user_version = ` + strconv.Itoa(2)); err != nil {
return err
}
return tx.Commit()