Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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