vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Add the workflow list and run cancel/rerun actions
0b26547e3ead573cfa604260d5f0bde83bcf18b0 · Keenan Nemetz · 15d ago · parent 455835e7
Add the workflow list and run cancel/rerun actions
The Runs tab now links to /r/{name}/workflows, which reads the default branch with the same validator the push boundary uses and shows triggers, jobs, runner labels, image, steps, outputs, and each job's latest run.
Maintainers can cancel a queued run or rerun a finished one. Rerun is another attempt on the same queue row, not a second row: the queue deliberately has one run per (repo, ref, sha, workflow, job), and attempts already carry their own logs and attestations. Running jobs are not interrupted. Both actions carry CSRF and are audited.
ci/runs/runs.go +57
| @@ -391,6 +391,63 @@ func (s *Store) RenameRepo(oldName, newName string) error { | |||
| 391 | 391 | return tx.Commit() | |
| 392 | 392 | } | |
| 393 | 393 | ||
| 394 | // CancelQueued retires one waiting run. Running work is not interrupted: the | ||
| 395 | // runner holding the lease has already started, and the queue cannot stop a | ||
| 396 | // process it does not own. | ||
| 397 | func (s *Store) CancelQueued(id, repo string, now time.Time) (bool, error) { | ||
| 398 | res, err := s.db.Exec(`UPDATE runs SET status = ?, finished = ?, lease_owner = '', lease_until = 0 | ||
| 399 | WHERE id = ? AND repo = ? AND status = ?`, | ||
| 400 | StatusCanceled, now.Unix(), id, repo, StatusQueued) | ||
| 401 | if err != nil { | ||
| 402 | return false, fmt.Errorf("runs: cancel %s: %w", id, err) | ||
| 403 | } | ||
| 404 | n, err := res.RowsAffected() | ||
| 405 | if err != nil { | ||
| 406 | return false, fmt.Errorf("runs: cancel %s: %w", id, err) | ||
| 407 | } | ||
| 408 | return n > 0, nil | ||
| 409 | } | ||
| 410 | |||
| 411 | // Requeue puts a finished run back on the queue as another attempt. It is a | ||
| 412 | // rerun of the same pinned job, not a new row: the unique index keeps one run per | ||
| 413 | // (repo, ref, sha, workflow, job), and attempts already have their own logs and | ||
| 414 | // attestations. Attempts is left alone so the cap still applies. | ||
| 415 | func (s *Store) Requeue(id, repo string, now time.Time) (bool, error) { | ||
| 416 | res, err := s.db.Exec(`UPDATE runs SET status = ?, created = ?, started = 0, finished = 0, | ||
| 417 | exit_code = -1, log = '', lease_owner = '', lease_until = 0 | ||
| 418 | WHERE id = ? AND repo = ? AND status IN (?, ?, ?)`, | ||
| 419 | StatusQueued, now.Unix(), id, repo, StatusSuccess, StatusFailed, StatusCanceled) | ||
| 420 | if err != nil { | ||
| 421 | return false, fmt.Errorf("runs: requeue %s: %w", id, err) | ||
| 422 | } | ||
| 423 | n, err := res.RowsAffected() | ||
| 424 | if err != nil { | ||
| 425 | return false, fmt.Errorf("runs: requeue %s: %w", id, err) | ||
| 426 | } | ||
| 427 | if n > 0 { | ||
| 428 | s.notify() | ||
| 429 | } | ||
| 430 | return n > 0, nil | ||
| 431 | } | ||
| 432 | |||
| 433 | // Labels returns the runner labels pinned with a run. | ||
| 434 | func (s *Store) Labels(id string) ([]string, error) { | ||
| 435 | rows, err := s.db.Query(`SELECT label FROM run_labels WHERE run_id=? ORDER BY label`, id) | ||
| 436 | if err != nil { | ||
| 437 | return nil, fmt.Errorf("runs: labels: %w", err) | ||
| 438 | } | ||
| 439 | defer rows.Close() | ||
| 440 | var out []string | ||
| 441 | for rows.Next() { | ||
| 442 | var label string | ||
| 443 | if err := rows.Scan(&label); err != nil { | ||
| 444 | return nil, fmt.Errorf("runs: labels: %w", err) | ||
| 445 | } | ||
| 446 | out = append(out, label) | ||
| 447 | } | ||
| 448 | return out, rows.Err() | ||
| 449 | } | ||
| 450 | |||
| 394 | 451 | // CancelSuperseded retires the queued runs of a ref that a newer tip replaced. Only | |
| 395 | 452 | // waiting rows are touched: a job that is already running is not interrupted, because | |
| 396 | 453 | // a canceled build is a different promise from a build nobody started. The caller | |