mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-30 00:23:45 +09:00
enhance(actions): add pending job status and align job statuses with GitHub (#39376)
This commit is contained in:
@@ -3072,7 +3072,7 @@ LEVEL = Info
|
||||
;ZOMBIE_TASK_TIMEOUT = 10m
|
||||
;; Timeout to stop the tasks which have running status and continuous updates, but don't end for a long time
|
||||
;ENDLESS_TASK_TIMEOUT = 3h
|
||||
;; Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time
|
||||
;; Timeout to cancel the jobs which haven't started and haven't been updated for a long time
|
||||
;ABANDONED_JOB_TIMEOUT = 24h
|
||||
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
|
||||
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
|
||||
|
||||
@@ -622,7 +622,7 @@ func RefreshReusableCallerStatus(ctx context.Context, caller *ActionRunJob) erro
|
||||
func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
allSuccessOrSkipped := len(jobs) != 0
|
||||
allSkipped := len(jobs) != 0
|
||||
var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked bool
|
||||
var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked, hasPending bool
|
||||
for _, job := range jobs {
|
||||
// A failed job with continue-on-error:true does not fail the workflow run.
|
||||
// It counts as a "continued failure" and is treated like success for aggregation.
|
||||
@@ -635,6 +635,7 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
hasWaiting = hasWaiting || job.Status == StatusWaiting
|
||||
hasRunning = hasRunning || job.Status == StatusRunning
|
||||
hasBlocked = hasBlocked || job.Status == StatusBlocked
|
||||
hasPending = hasPending || job.Status == StatusPending
|
||||
}
|
||||
switch {
|
||||
case allSkipped:
|
||||
@@ -648,9 +649,11 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
case hasWaiting:
|
||||
return StatusWaiting
|
||||
case hasBlocked:
|
||||
// Blocked is still a pending state, so it should outrank terminal
|
||||
// Blocked is still an unfinished state, so it should outrank terminal
|
||||
// statuses like cancelled/failure when no job is waiting or running.
|
||||
return StatusBlocked
|
||||
case hasPending:
|
||||
return StatusRunning // a run with only pending jobs left is still in progress
|
||||
case hasCancelled:
|
||||
if hasFailure && hasFailFastMatrixFailure(jobs) {
|
||||
return StatusFailure
|
||||
|
||||
@@ -76,6 +76,8 @@ func TestAggregateJobStatus(t *testing.T) {
|
||||
{[]Status{StatusFailure, StatusWaiting}, StatusWaiting},
|
||||
{[]Status{StatusFailure, StatusRunning}, StatusRunning},
|
||||
{[]Status{StatusFailure, StatusBlocked}, StatusBlocked},
|
||||
{[]Status{StatusFailure, StatusPending}, StatusRunning},
|
||||
{[]Status{StatusBlocked, StatusPending}, StatusBlocked},
|
||||
|
||||
// skipped with other status
|
||||
// "all skipped" is also considered as "mergeable" by "services/actions.toCommitStatus", the same as GitHub
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
StatusRunning // 6, isn't a runnerv1.Result
|
||||
StatusBlocked // 7, isn't a runnerv1.Result
|
||||
StatusCancelling // 8, isn't a runnerv1.Result
|
||||
StatusPending // 9, isn't a runnerv1.Result
|
||||
)
|
||||
|
||||
var statusNames = map[Status]string{
|
||||
@@ -35,6 +36,7 @@ var statusNames = map[Status]string{
|
||||
StatusCancelling: "cancelling",
|
||||
StatusSkipped: "skipped",
|
||||
StatusBlocked: "blocked",
|
||||
StatusPending: "pending",
|
||||
}
|
||||
|
||||
// String returns the string name of the Status
|
||||
@@ -93,6 +95,10 @@ func (s Status) IsCancelling() bool {
|
||||
return s == StatusCancelling
|
||||
}
|
||||
|
||||
func (s Status) IsPending() bool {
|
||||
return s == StatusPending
|
||||
}
|
||||
|
||||
// In returns whether s is one of the given statuses
|
||||
func (s Status) In(statuses ...Status) bool {
|
||||
return slices.Contains(statuses, s)
|
||||
|
||||
@@ -3783,6 +3783,7 @@
|
||||
"actions.status.cancelling": "Canceling",
|
||||
"actions.status.skipped": "Skipped",
|
||||
"actions.status.blocked": "Blocked",
|
||||
"actions.status.pending": "Pending",
|
||||
"actions.runners": "Runners",
|
||||
"actions.runners.runner_manage_panel": "Runners Management",
|
||||
"actions.runners.new": "Create new Runner",
|
||||
|
||||
@@ -18,7 +18,7 @@ func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
@@ -70,7 +70,7 @@ func ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
|
||||
@@ -581,7 +581,7 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
@@ -626,7 +626,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
|
||||
@@ -673,7 +673,7 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
@@ -737,7 +737,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
@@ -984,7 +984,7 @@ func ActionsListWorkflowRuns(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
@@ -1624,7 +1624,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
@@ -1701,7 +1701,7 @@ func ListWorkflowRunAttemptJobs(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
|
||||
@@ -120,7 +120,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI
|
||||
func convertToInternal(s string) ([]actions_model.Status, error) {
|
||||
switch s {
|
||||
case "pending", "waiting", "requested", "action_required":
|
||||
return []actions_model.Status{actions_model.StatusBlocked}, nil
|
||||
return []actions_model.Status{actions_model.StatusBlocked, actions_model.StatusPending}, nil
|
||||
case "queued":
|
||||
return []actions_model.Status{actions_model.StatusWaiting}, nil
|
||||
case "in_progress":
|
||||
|
||||
@@ -352,7 +352,7 @@ func ListWorkflowRuns(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: actor
|
||||
@@ -393,7 +393,7 @@ func ListWorkflowJobs(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: status
|
||||
// in: query
|
||||
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
|
||||
// description: workflow status (requested, pending, queued, in_progress, failure, success, skipped)
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: page
|
||||
|
||||
@@ -556,7 +556,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
|
||||
{
|
||||
ID: finalID, Link: jobLink(finalID), JobID: "final", Name: "final",
|
||||
Status: actions_model.StatusBlocked.String(), Duration: "0s",
|
||||
Status: actions_model.StatusPending.String(), Duration: "0s",
|
||||
Needs: []string{"local_caller", "cross_caller"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -556,7 +556,7 @@ func (data *actionRunListData) processActionRuns(ctx *context.Context) bool {
|
||||
for _, job := range jobs {
|
||||
// A deferred matrix is unresolvable until its needs finish, so the whole per-job block
|
||||
// is skipped: parsing the payload would report a valid workflow as invalid.
|
||||
if job.IsMatrixDeferred || !job.Status.In(actions_model.StatusWaiting, actions_model.StatusBlocked) {
|
||||
if job.IsMatrixDeferred || !job.Status.In(actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusPending) {
|
||||
continue
|
||||
}
|
||||
if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil {
|
||||
|
||||
@@ -766,14 +766,10 @@ func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewRespon
|
||||
}
|
||||
}
|
||||
|
||||
// describePendingJobDetail explains why a blocked or waiting job has not started
|
||||
// yet, so the user can tell whether it is waiting on its dependencies or on an
|
||||
// available runner. It returns an empty string when the job is not pending or the
|
||||
// cause can't be determined (the caller keeps the generic status label then).
|
||||
// describePendingJobDetail explains why a pending or waiting job has not started, or returns an empty string when it can't tell
|
||||
func describePendingJobDetail(ctx *context_module.Context, current *actions_model.ActionRunJob, jobs []*actions_model.ActionRunJob) string {
|
||||
switch {
|
||||
case current.Status.IsBlocked():
|
||||
// A blocked job is held back by the jobs listed in its `needs`.
|
||||
case current.Status.IsPending():
|
||||
if pending := pendingNeeds(current, jobs); len(pending) > 0 {
|
||||
return ctx.Locale.TrString("actions.runs.waiting_for_dependent_jobs", strings.Join(pending, ", "))
|
||||
}
|
||||
@@ -852,7 +848,7 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
|
||||
viewJobs = append(viewJobs, &ViewJobStep{
|
||||
Summary: v.Name,
|
||||
Duration: v.Duration().String(),
|
||||
Status: status.String(),
|
||||
Status: util.Iif(status.IsWaiting(), actions_model.StatusPending, status).String(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestConvertToViewModelCancellingTaskDoesNotRenderRunningSteps(t *testing.T)
|
||||
{
|
||||
Summary: "Complete job",
|
||||
Duration: "0s",
|
||||
Status: "waiting",
|
||||
Status: "pending",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||
|
||||
@@ -174,7 +174,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
|
||||
// CancelAbandonedJobs cancels jobs that have not been picked by any runner for a long time
|
||||
func CancelAbandonedJobs(ctx context.Context) error {
|
||||
abandonedJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
|
||||
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
|
||||
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusPending},
|
||||
UpdatedBefore: timeutil.TimeStampNow().AddDuration(-setting.Actions.AbandonedJobTimeout),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -53,6 +53,10 @@ func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.Action
|
||||
scopedPrefix = actions_model.ScopedStatusContextPrefix(ctx, run.WorkflowRepoID)
|
||||
}
|
||||
|
||||
var pendingFilter *pendingJobFilter
|
||||
if slices.ContainsFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.Status.IsPending() && !job.IsMatrixDeferred }) {
|
||||
pendingFilter = newPendingJobFilter(ctx, run)
|
||||
}
|
||||
for _, job := range jobs {
|
||||
// A deferred-matrix placeholder's name changes when it expands, so a status created while it
|
||||
// waits would be orphaned. The emitter reloads the jobs after expanding and creates them
|
||||
@@ -61,7 +65,7 @@ func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.Action
|
||||
if job.IsMatrixDeferred && !job.Status.IsDone() {
|
||||
continue
|
||||
}
|
||||
if err = createCommitStatus(ctx, run.Repo, event, commitID, scopedPrefix, run, job); err != nil {
|
||||
if err = createCommitStatus(ctx, run.Repo, event, commitID, scopedPrefix, run, job, pendingFilter); err != nil {
|
||||
log.Error("Failed to create commit status for job %d: %v", job.ID, err)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +155,7 @@ func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, c
|
||||
return event, commitID, nil
|
||||
}
|
||||
|
||||
func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, commitID, scopedPrefix string, run *actions_model.ActionRun, job *actions_model.ActionRunJob) error {
|
||||
func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, commitID, scopedPrefix string, run *actions_model.ActionRun, job *actions_model.ActionRunJob, pendingFilter *pendingJobFilter) error {
|
||||
displayName := actions_module.WorkflowDisplayName(run.WorkflowID, job.WorkflowPayload)
|
||||
ctxName := actions_module.WorkflowStatusContextName(displayName, job.Name, event) // git_model.NewCommitStatus also trims spaces
|
||||
if run.IsScopedRun {
|
||||
@@ -160,7 +164,35 @@ func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event,
|
||||
ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, job.Name, event)
|
||||
}
|
||||
targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID)
|
||||
return createWorkflowCommitStatus(ctx, repo, commitID, ctxName, run.WorkflowID, toCommitStatus(job.Status), targetURL, toCommitStatusDescription(job))
|
||||
return createWorkflowCommitStatus(ctx, repo, commitID, ctxName, run.WorkflowID, toCommitStatus(job.Status), targetURL, toCommitStatusDescription(job), pendingFilter.onlyReplace(job, ctxName))
|
||||
}
|
||||
|
||||
// pendingJobFilter keeps optional Pending jobs from posting new statuses
|
||||
type pendingJobFilter struct {
|
||||
requiredGlobs []glob.Glob
|
||||
}
|
||||
|
||||
func newPendingJobFilter(ctx context.Context, run *actions_model.ActionRun) *pendingJobFilter {
|
||||
rules, err := git_model.FindRepoProtectedBranchRules(ctx, run.RepoID)
|
||||
if err != nil {
|
||||
log.Error("FindRepoProtectedBranchRules: %v", err)
|
||||
return nil
|
||||
}
|
||||
if slices.ContainsFunc(rules, func(rule *git_model.ProtectedBranch) bool {
|
||||
return rule.EnableStatusCheck && len(rule.StatusCheckContexts) == 0
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
requiredGlobs, err := requiredStatusContextGlobs(ctx, run.Repo, rules)
|
||||
if err != nil {
|
||||
log.Error("requiredStatusContextGlobs: %v", err)
|
||||
return nil
|
||||
}
|
||||
return &pendingJobFilter{requiredGlobs: requiredGlobs}
|
||||
}
|
||||
|
||||
func (f *pendingJobFilter) onlyReplace(job *actions_model.ActionRunJob, ctxName string) bool {
|
||||
return f != nil && job.Status.IsPending() && !slices.ContainsFunc(f.requiredGlobs, func(gp glob.Glob) bool { return gp.Match(ctxName) })
|
||||
}
|
||||
|
||||
// getAllRequiredStatusContextGlobs returns the compiled globs of every status-check context required in the repo:
|
||||
@@ -170,6 +202,10 @@ func getAllRequiredStatusContextGlobs(ctx context.Context, repo *repo_model.Repo
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FindRepoProtectedBranchRules: %w", err)
|
||||
}
|
||||
return requiredStatusContextGlobs(ctx, repo, rules)
|
||||
}
|
||||
|
||||
func requiredStatusContextGlobs(ctx context.Context, repo *repo_model.Repository, rules git_model.ProtectedBranchRules) ([]glob.Glob, error) {
|
||||
required, err := pull_service.EffectiveRequiredContexts(ctx, repo, rules...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("EffectiveRequiredContexts: %w", err)
|
||||
@@ -246,7 +282,7 @@ func CreateSkippedCommitStatusForFilteredWorkflow(ctx context.Context, repo *rep
|
||||
continue
|
||||
}
|
||||
// "Skipped" mirrors toCommitStatusDescription for StatusSkipped.
|
||||
if err := createWorkflowCommitStatus(ctx, repo, commitID, ctxName, workflowID, commitstatus.CommitStatusSkipped, "", "Skipped"); err != nil {
|
||||
if err := createWorkflowCommitStatus(ctx, repo, commitID, ctxName, workflowID, commitstatus.CommitStatusSkipped, "", "Skipped", false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -254,7 +290,7 @@ func CreateSkippedCommitStatusForFilteredWorkflow(ctx context.Context, repo *rep
|
||||
}
|
||||
|
||||
// createWorkflowCommitStatus posts the commit status for one workflow-job context.
|
||||
func createWorkflowCommitStatus(ctx context.Context, repo *repo_model.Repository, commitID, ctxName, workflowID string, state commitstatus.CommitStatusState, targetURL, description string) error {
|
||||
func createWorkflowCommitStatus(ctx context.Context, repo *repo_model.Repository, commitID, ctxName, workflowID string, state commitstatus.CommitStatusState, targetURL, description string, onlyReplace bool) error {
|
||||
// Mix the workflow file path into the hash so two workflow files that
|
||||
// share the same `name:` and job name produce distinct commit statuses
|
||||
// even though they render identically — matching GitHub's behavior
|
||||
@@ -280,14 +316,19 @@ func createWorkflowCommitStatus(ctx context.Context, repo *repo_model.Repository
|
||||
break
|
||||
}
|
||||
}
|
||||
hasPrevious := false
|
||||
for _, v := range statuses {
|
||||
if v.ContextHash == ctxHash {
|
||||
if v.State == state && v.TargetURL == targetURL && v.Description == description {
|
||||
return nil
|
||||
}
|
||||
hasPrevious = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if onlyReplace && !hasPrevious {
|
||||
return nil
|
||||
}
|
||||
|
||||
creator := user_model.NewActionsUser()
|
||||
status := git_model.CommitStatus{
|
||||
@@ -322,6 +363,8 @@ func toCommitStatusDescription(job *actions_model.ActionRunJob) string {
|
||||
return "Waiting to run"
|
||||
case actions_model.StatusBlocked:
|
||||
return "Blocked by required conditions"
|
||||
case actions_model.StatusPending:
|
||||
return "Waiting for needed jobs"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown status: %d", job.Status)
|
||||
}
|
||||
@@ -333,7 +376,7 @@ func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState
|
||||
return commitstatus.CommitStatusSuccess
|
||||
case actions_model.StatusFailure, actions_model.StatusCancelled:
|
||||
return commitstatus.CommitStatusFailure
|
||||
case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning, actions_model.StatusCancelling:
|
||||
case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusPending, actions_model.StatusRunning, actions_model.StatusCancelling:
|
||||
return commitstatus.CommitStatusPending
|
||||
case actions_model.StatusSkipped:
|
||||
return commitstatus.CommitStatusSkipped
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestCommitStatusDescription(t *testing.T) {
|
||||
{actions_model.StatusRunning, 0, 0, "In progress"},
|
||||
{actions_model.StatusWaiting, 0, 0, "Waiting to run"},
|
||||
{actions_model.StatusBlocked, 0, 0, "Blocked by required conditions"},
|
||||
{actions_model.StatusPending, 0, 0, "Waiting for needed jobs"},
|
||||
{actions_model.StatusUnknown, 0, 0, "Unknown status: 0"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -71,7 +72,7 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) {
|
||||
expectedContext := "status-dedupe-test.yaml / status-dedupe-job (push)"
|
||||
expectedTargetURL := run.Link() + "/jobs/99002"
|
||||
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job, nil))
|
||||
|
||||
statuses := findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext)
|
||||
require.Len(t, statuses, 1)
|
||||
@@ -80,7 +81,7 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) {
|
||||
assert.Equal(t, expectedTargetURL, statuses[0].TargetURL)
|
||||
|
||||
job.Status = actions_model.StatusRunning
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job, nil))
|
||||
|
||||
statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext)
|
||||
require.Len(t, statuses, 2)
|
||||
@@ -89,17 +90,46 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) {
|
||||
assert.Equal(t, "In progress", statuses[1].Description)
|
||||
assert.Equal(t, expectedTargetURL, statuses[1].TargetURL)
|
||||
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job, nil))
|
||||
statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext)
|
||||
assert.Len(t, statuses, 2)
|
||||
|
||||
job.Status = actions_model.StatusSuccess
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job, nil))
|
||||
statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext)
|
||||
require.Len(t, statuses, 3)
|
||||
assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State)
|
||||
}
|
||||
|
||||
func TestCreateCommitStatus_HidesOptionalPendingJobs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: repo.DefaultBranch})
|
||||
run := &actions_model.ActionRun{ID: 99101, RepoID: repo.ID, Repo: repo, WorkflowID: "ci.yaml"}
|
||||
deploy := &actions_model.ActionRunJob{ID: 99102, RunID: run.ID, RepoID: repo.ID, Name: "deploy", Status: actions_model.StatusPending}
|
||||
postDeploy := func(pending *pendingJobFilter) []*git_model.CommitStatus {
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, deploy, pending))
|
||||
return findCommitStatusesForContext(t, repo.ID, branch.CommitID, "ci.yaml / deploy (push)")
|
||||
}
|
||||
|
||||
pending := newPendingJobFilter(t.Context(), run)
|
||||
assert.Empty(t, postDeploy(pending))
|
||||
|
||||
deploy.Status = actions_model.StatusSuccess
|
||||
assert.Len(t, postDeploy(nil), 1)
|
||||
deploy.Status = actions_model.StatusPending
|
||||
assert.Len(t, postDeploy(pending), 2)
|
||||
|
||||
require.NoError(t, db.Insert(t.Context(), &git_model.ProtectedBranch{RepoID: repo.ID, RuleName: "main", EnableStatusCheck: true, StatusCheckContexts: []string{"ci.yaml / deploy*"}}))
|
||||
pending = newPendingJobFilter(t.Context(), run)
|
||||
assert.False(t, pending.onlyReplace(deploy, "ci.yaml / deploy (push)"))
|
||||
assert.True(t, pending.onlyReplace(deploy, "other / deploy (push)"))
|
||||
|
||||
require.NoError(t, db.Insert(t.Context(), &git_model.ProtectedBranch{RepoID: repo.ID, RuleName: "release", EnableStatusCheck: true}))
|
||||
assert.Nil(t, newPendingJobFilter(t.Context(), run))
|
||||
}
|
||||
|
||||
func TestGetCommitActionsStatusMap(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -125,7 +155,7 @@ func TestGetCommitActionsStatusMap(t *testing.T) {
|
||||
RunID: run.ID, RepoID: repo.ID, OwnerID: repo.OwnerID, Name: tc.jobName, Status: tc.status,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job, nil))
|
||||
}
|
||||
|
||||
statuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll)
|
||||
@@ -184,7 +214,7 @@ jobs:
|
||||
WorkflowPayload: payload,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "pull_request", branch.CommitID, "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "pull_request", branch.CommitID, "", run, job, nil))
|
||||
}
|
||||
|
||||
statuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll)
|
||||
@@ -242,7 +272,7 @@ func TestCreateCommitStatus_LegacyHashRecovery(t *testing.T) {
|
||||
Name: "my-job", Status: actions_model.StatusSuccess,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job, nil))
|
||||
|
||||
latest, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll)
|
||||
require.NoError(t, err)
|
||||
@@ -299,7 +329,7 @@ func TestCreateCommitStatus_LegacyHashExternalNotAdopted(t *testing.T) {
|
||||
Name: "my-job", Status: actions_model.StatusSuccess,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job, nil))
|
||||
|
||||
latest, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll)
|
||||
require.NoError(t, err)
|
||||
@@ -351,7 +381,7 @@ func TestCreateCommitStatus_UnnamedWorkflowUsesFileName(t *testing.T) {
|
||||
`),
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job, nil))
|
||||
|
||||
statuses := findCommitStatusesForContext(t, repo.ID, branch.CommitID, tc.workflowID+" / my-test (push)")
|
||||
require.Len(t, statuses, 1)
|
||||
@@ -406,7 +436,7 @@ jobs:
|
||||
if run.IsScopedRun {
|
||||
scopedPrefix = actions_model.ScopedStatusContextPrefix(t.Context(), run.WorkflowRepoID)
|
||||
}
|
||||
require.NoError(t, createCommitStatus(t.Context(), consumer, "push", branch.CommitID, scopedPrefix, run, job))
|
||||
require.NoError(t, createCommitStatus(t.Context(), consumer, "push", branch.CommitID, scopedPrefix, run, job, nil))
|
||||
}
|
||||
|
||||
// repo-level Context is the bare "<display name> / <job> (<event>)"; the scoped one is the same but sets off the source repo with a colon,
|
||||
|
||||
@@ -63,7 +63,7 @@ func handleInvalidWorkflows(ctx context.Context, input *notifyInput, ref git.Ref
|
||||
continue
|
||||
}
|
||||
if err := createWorkflowCommitStatus(ctx, run.Repo, run.CommitSHA, entryName+" ("+run.TriggerEvent+")", run.WorkflowID,
|
||||
commitstatus.CommitStatusFailure, run.Link(), "Invalid workflow file"); err != nil {
|
||||
commitstatus.CommitStatusFailure, run.Link(), "Invalid workflow file", false); err != nil {
|
||||
log.Error("create commit status for invalid workflow %q: %v", entryName, err)
|
||||
}
|
||||
NotifyWorkflowRunStatusUpdate(ctx, run)
|
||||
|
||||
@@ -244,9 +244,9 @@ func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (*jo
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// checkJobsOfCurrentRunAttempt resolves blocked jobs of the run's latest attempt.
|
||||
// checkJobsOfCurrentRunAttempt resolves pending and blocked jobs of the run's latest attempt.
|
||||
func checkJobsOfCurrentRunAttempt(ctx context.Context, run *actions_model.ActionRun) (*jobsCheckResult, error) {
|
||||
// Approval is the only transition allowed to release an approval-pending run.
|
||||
// Approval is the only transition allowed to release a run awaiting approval.
|
||||
if run.NeedApproval {
|
||||
return &jobsCheckResult{}, nil
|
||||
}
|
||||
@@ -366,7 +366,7 @@ func checkJobsOfCurrentRunAttempt(ctx context.Context, run *actions_model.Action
|
||||
}
|
||||
|
||||
result.UpdatedJobs = append(result.UpdatedJobs, resolver.matrixUpdatedJobs...)
|
||||
// Caller and matrix expansion both insert Blocked jobs, which only a follow-up pass resolves.
|
||||
// Caller and matrix expansion both insert Pending or Blocked jobs, which only a follow-up pass resolves.
|
||||
// Like the caller's children, matrix siblings are left out of result.Jobs and picked up there.
|
||||
if expandedAnyCaller || resolver.matrixChanged {
|
||||
result.RunIDsToReEmit = append(result.RunIDsToReEmit, run.ID)
|
||||
@@ -398,7 +398,7 @@ func cancelFailedMatrixSiblings(ctx context.Context, jobs actions_model.ActionJo
|
||||
|
||||
type jobStatusResolver struct {
|
||||
statuses map[int64]actions_model.Status
|
||||
// sortedIDs are the keys of statuses, so blocked jobs are resolved in insertion order.
|
||||
// sortedIDs are the keys of statuses, so jobs are resolved in insertion order.
|
||||
// Resolve only ever rewrites statuses values, never its key set.
|
||||
sortedIDs []int64
|
||||
needs map[int64][]int64
|
||||
@@ -498,6 +498,20 @@ func (r *jobStatusResolver) resolveCheckNeeds(id int64) (allDone, allSucceed boo
|
||||
return allDone, allSucceed
|
||||
}
|
||||
|
||||
func (r *jobStatusResolver) updateStatus(ctx context.Context, job *actions_model.ActionRunJob, status actions_model.Status) error {
|
||||
cond := builder.Eq{"status": job.Status}
|
||||
job.Status = status
|
||||
affected, err := actions_model.UpdateRunJob(ctx, job, cond, "status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected != 1 {
|
||||
return fmt.Errorf("no affected for updating job %d", job.ID)
|
||||
}
|
||||
r.statuses[job.ID] = status
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *jobStatusResolver) resolve(ctx context.Context) (map[int64]actions_model.Status, error) {
|
||||
ret := map[int64]actions_model.Status{}
|
||||
|
||||
@@ -509,7 +523,7 @@ func (r *jobStatusResolver) resolve(ctx context.Context) (map[int64]actions_mode
|
||||
for _, id := range r.sortedIDs {
|
||||
status := r.statuses[id]
|
||||
actionRunJob := r.jobMap[id]
|
||||
if status != actions_model.StatusBlocked {
|
||||
if !status.In(actions_model.StatusPending, actions_model.StatusBlocked) {
|
||||
continue
|
||||
}
|
||||
// An expanded caller has been resolved in an earlier pass, skip.
|
||||
@@ -522,7 +536,18 @@ func (r *jobStatusResolver) resolve(ctx context.Context) (map[int64]actions_mode
|
||||
continue
|
||||
}
|
||||
}
|
||||
// past the run's own holds, a job is Pending exactly while its needs are unfinished
|
||||
allDone, allSucceed := r.resolveCheckNeeds(id)
|
||||
var err error
|
||||
switch {
|
||||
case status.IsPending() && allDone:
|
||||
err = r.updateStatus(ctx, actionRunJob, actions_model.StatusBlocked)
|
||||
case status.IsBlocked() && !allDone:
|
||||
err = r.updateStatus(ctx, actionRunJob, actions_model.StatusPending)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !allDone {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple blocked",
|
||||
name: "multiple pending",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "1", Status: actions_model.StatusSuccess, Needs: []string{}},
|
||||
{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
|
||||
{ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
|
||||
{ID: 2, JobID: "2", Status: actions_model.StatusPending, Needs: []string{"1"}},
|
||||
{ID: 3, JobID: "3", Status: actions_model.StatusPending, Needs: []string{"1"}},
|
||||
},
|
||||
want: map[int64]actions_model.Status{
|
||||
2: actions_model.StatusWaiting,
|
||||
@@ -70,11 +70,11 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "chain blocked",
|
||||
name: "chain pending",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||
{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
|
||||
{ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"2"}},
|
||||
{ID: 2, JobID: "2", Status: actions_model.StatusPending, Needs: []string{"1"}},
|
||||
{ID: 3, JobID: "3", Status: actions_model.StatusPending, Needs: []string{"2"}},
|
||||
},
|
||||
want: map[int64]actions_model.Status{
|
||||
2: actions_model.StatusSkipped,
|
||||
@@ -93,9 +93,9 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
|
||||
{
|
||||
name: "loop need",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "1", Status: actions_model.StatusBlocked, Needs: []string{"3"}},
|
||||
{ID: 2, JobID: "2", Status: actions_model.StatusBlocked, Needs: []string{"1"}},
|
||||
{ID: 3, JobID: "3", Status: actions_model.StatusBlocked, Needs: []string{"2"}},
|
||||
{ID: 1, JobID: "1", Status: actions_model.StatusPending, Needs: []string{"3"}},
|
||||
{ID: 2, JobID: "2", Status: actions_model.StatusPending, Needs: []string{"1"}},
|
||||
{ID: 3, JobID: "3", Status: actions_model.StatusPending, Needs: []string{"2"}},
|
||||
},
|
||||
want: map[int64]actions_model.Status{},
|
||||
},
|
||||
@@ -103,7 +103,7 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
|
||||
name: "`if` is not empty and all jobs in `needs` completed successfully",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusSuccess, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusPending, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
`
|
||||
name: test
|
||||
on: push
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
name: "`if` is not empty and not all jobs in `needs` completed successfully",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusPending, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
`
|
||||
name: test
|
||||
on: push
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
name: "`if` is empty and not all jobs in `needs` completed successfully",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusPending, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
`
|
||||
name: test
|
||||
on: push
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
name: "`if` is empty and a failed need has continue-on-error",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, ContinueOnError: true, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusPending, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
`
|
||||
name: test
|
||||
on: push
|
||||
@@ -263,7 +263,7 @@ jobs:
|
||||
},
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusSuccess, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusPending, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
`
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -288,10 +288,13 @@ jobs:
|
||||
for i, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Each subtest gets a unique RunID / RunAttemptID so jobs from different subtests don't bleed into each other's FindTaskNeeds queries
|
||||
runID := int64(9001 + i)
|
||||
attemptID := int64(9001 + i)
|
||||
repoID := tt.jobs[0].RepoID
|
||||
dbRun := &actions_model.ActionRun{RepoID: repoID, Index: int64(9001 + i)}
|
||||
require.NoError(t, db.Insert(ctx, dbRun))
|
||||
attempt := &actions_model.ActionRunAttempt{RepoID: repoID, RunID: dbRun.ID, Attempt: 1}
|
||||
require.NoError(t, db.Insert(ctx, attempt))
|
||||
runID, attemptID := dbRun.ID, attempt.ID
|
||||
run := util.IfZero(tt.run, stubRun)
|
||||
require.NoError(t, db.Insert(ctx, &actions_model.ActionRunAttempt{ID: attemptID, RepoID: 1, RunID: runID}))
|
||||
|
||||
// Insert each test job (letting the DB assign IDs) and remember the testID -> dbID mapping so we can translate the expected map.
|
||||
idMap := make(map[int64]int64, len(tt.jobs))
|
||||
@@ -302,9 +305,7 @@ jobs:
|
||||
j.RunAttemptID = attemptID
|
||||
j.Run = run
|
||||
|
||||
// The resolver evaluates Blocked jobs via evaluateJobIf, which needs a valid YAML payload;
|
||||
// supply a minimal one when the case didn't.
|
||||
if j.Status == actions_model.StatusBlocked && len(j.WorkflowPayload) == 0 {
|
||||
if j.Status.In(actions_model.StatusPending, actions_model.StatusBlocked) && len(j.WorkflowPayload) == 0 {
|
||||
j.WorkflowPayload = minimalWorkflowPayload(j.JobID)
|
||||
}
|
||||
|
||||
@@ -764,34 +765,44 @@ func Test_maxParallelReusableCallerLifecycle(t *testing.T) {
|
||||
// dependents honest: a round resolved after an insert would judge them against a job set that is
|
||||
// missing the siblings. See Resolve for why that is wrong.
|
||||
func Test_jobStatusResolverStopsAfterMatrixInsert(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
// build (2) stands for the expanded anchor: it reaches a terminal status this round, which is
|
||||
// what would let report (3) resolve in the next one.
|
||||
newChain := func() actions_model.ActionJobList {
|
||||
return actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "generate", Status: actions_model.StatusFailure, WorkflowPayload: minimalWorkflowPayload("generate")},
|
||||
{ID: 2, JobID: "build", Status: actions_model.StatusBlocked, Needs: []string{"generate"}, WorkflowPayload: minimalWorkflowPayload("build")},
|
||||
{ID: 3, JobID: "report", Status: actions_model.StatusBlocked, Needs: []string{"build"}, WorkflowPayload: minimalWorkflowPayload("report")},
|
||||
newChain := func(index int64) actions_model.ActionJobList {
|
||||
run := &actions_model.ActionRun{Index: index}
|
||||
require.NoError(t, db.Insert(ctx, run))
|
||||
attempt := &actions_model.ActionRunAttempt{RunID: run.ID, Attempt: 1}
|
||||
require.NoError(t, db.Insert(ctx, attempt))
|
||||
jobs := actions_model.ActionJobList{
|
||||
{JobID: "generate", Status: actions_model.StatusFailure, WorkflowPayload: minimalWorkflowPayload("generate")},
|
||||
{JobID: "build", Status: actions_model.StatusPending, Needs: []string{"generate"}, WorkflowPayload: minimalWorkflowPayload("build")},
|
||||
{JobID: "report", Status: actions_model.StatusPending, Needs: []string{"build"}, WorkflowPayload: minimalWorkflowPayload("report")},
|
||||
}
|
||||
for _, job := range jobs {
|
||||
job.RunID, job.RunAttemptID = run.ID, attempt.ID
|
||||
require.NoError(t, db.Insert(ctx, job))
|
||||
}
|
||||
return jobs
|
||||
}
|
||||
|
||||
t.Run("without an insert the whole chain resolves in one pass", func(t *testing.T) {
|
||||
got, err := newJobStatusResolver(newChain(), nil).Resolve(ctx)
|
||||
chain := newChain(9201)
|
||||
got, err := newJobStatusResolver(chain, nil).Resolve(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[int64]actions_model.Status{
|
||||
2: actions_model.StatusSkipped,
|
||||
3: actions_model.StatusSkipped,
|
||||
chain[1].ID: actions_model.StatusSkipped,
|
||||
chain[2].ID: actions_model.StatusSkipped,
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("an insert stops the pass before the dependents are resolved", func(t *testing.T) {
|
||||
r := newJobStatusResolver(newChain(), nil)
|
||||
chain := newChain(9202)
|
||||
r := newJobStatusResolver(chain, nil)
|
||||
r.matrixInserted = true // as resolve() sets it once expansion has inserted siblings
|
||||
|
||||
got, err := r.Resolve(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[int64]actions_model.Status{2: actions_model.StatusSkipped}, got,
|
||||
assert.Equal(t, map[int64]actions_model.Status{chain[1].ID: actions_model.StatusSkipped}, got,
|
||||
"report must wait for the re-emit, which sees the sibling combinations too")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
@@ -103,6 +104,52 @@ func TestPrepareRunAndInsert_MaxParallel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckJobs_ApprovedRunWaitsOnNeedsThenMaxParallel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&EmitJobsIfReadyByRun, func(int64) error { return nil })()
|
||||
|
||||
run := insertMaxParallelRun(t, `name: max-parallel
|
||||
on: push
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
build:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- run: echo hi
|
||||
`, true)
|
||||
assert.Equal(t, map[actions_model.Status]int{actions_model.StatusBlocked: 4}, statusCounts(runJobs(t, run.ID, run.LatestAttemptID)))
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: run.RepoID})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
_, err := ApproveRuns(t.Context(), repo, doer, []int64{run.ID})
|
||||
require.NoError(t, err)
|
||||
checkJobs := func() actions_model.ActionJobList {
|
||||
_, err := checkJobsOfCurrentRunAttempt(t.Context(), unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID}))
|
||||
require.NoError(t, err)
|
||||
return runJobs(t, run.ID, run.LatestAttemptID)
|
||||
}
|
||||
jobs := checkJobs()
|
||||
assert.Equal(t, map[actions_model.Status]int{actions_model.StatusWaiting: 1, actions_model.StatusPending: 3}, statusCounts(jobs))
|
||||
|
||||
setup := jobs[slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.JobID == "setup" })]
|
||||
setup.Status = actions_model.StatusSuccess
|
||||
_, err = actions_model.UpdateRunJob(t.Context(), setup, nil, "status")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[actions_model.Status]int{
|
||||
actions_model.StatusSuccess: 1,
|
||||
actions_model.StatusWaiting: 2,
|
||||
actions_model.StatusBlocked: 1,
|
||||
}, statusCounts(checkJobs()))
|
||||
}
|
||||
|
||||
// A reusable workflow declares its own strategy, so the limit must reach the child jobs.
|
||||
func TestInsertCallerChildren_MaxParallel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -28,7 +28,6 @@ func NotifyWorkflowJobsAndRunsStatusUpdate(ctx context.Context, jobs []*actions_
|
||||
log.Error("Failed to load job attributes: %v", err)
|
||||
continue
|
||||
}
|
||||
CreateCommitStatusForRunJobs(ctx, job.Run, job)
|
||||
|
||||
runRepoIDs[job.RunID] = job.RepoID
|
||||
if _, ok := jobsByRunID[job.RunID]; !ok {
|
||||
@@ -42,6 +41,7 @@ func NotifyWorkflowJobsAndRunsStatusUpdate(ctx context.Context, jobs []*actions_
|
||||
}
|
||||
|
||||
for _, jobs := range jobsByRunID {
|
||||
CreateCommitStatusForRunJobs(ctx, jobs[0].Run, jobs...)
|
||||
NotifyWorkflowJobsStatusUpdate(ctx, jobs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +286,10 @@ func execRerunPlan(ctx context.Context, plan *rerunPlan) (*actions_model.ActionR
|
||||
var invalidIf error
|
||||
if plan.rerunAttemptJobIDs.Contains(templateJob.AttemptJobID) {
|
||||
// the emitter decides `if:` once all needs have results, and is the only place expanding a deferred matrix
|
||||
shouldBlockJob := shouldBlock || len(newJob.Needs) > 0 || newJob.IsMatrixDeferred
|
||||
|
||||
newJob.Status = util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
||||
newJob.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
||||
if newJob.Status.IsWaiting() && len(newJob.Needs) > 0 {
|
||||
newJob.Status = actions_model.StatusPending
|
||||
}
|
||||
newJob.TaskID = 0
|
||||
newJob.SourceTaskID = 0
|
||||
newJob.Started = 0
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
"xorm.io/builder"
|
||||
@@ -187,7 +188,7 @@ func canonicalCallUses(job *actions_model.ActionRunJob) string {
|
||||
}
|
||||
|
||||
// expandReusableWorkflowCaller loads and parses the target reusable workflow and inserts the caller's direct child jobs.
|
||||
// It expands only ONE level: a child that is itself a reusable caller is inserted Blocked and expanded later by a subsequent resolver pass.
|
||||
// It expands only ONE level: a child that is itself a reusable caller is inserted Blocked or Pending and expanded later by a subsequent resolver pass.
|
||||
// It does NOT schedule a follow-up resolver pass; the caller of this function is responsible for emitting.
|
||||
//
|
||||
// All call sites (PrepareRunAndInsert, execRerunPlan, checkJobsOfCurrentRunAttempt, ApproveRuns) invoke this inside their enclosing write transaction,
|
||||
@@ -404,7 +405,7 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att
|
||||
RunsOn: parsedChild.RunsOn(),
|
||||
ContinueOnError: parsedChild.GetContinueOnError(),
|
||||
MaxParallel: parseMaxParallel(jobID, parsedChild.Strategy.MaxParallelString),
|
||||
Status: actions_model.StatusBlocked,
|
||||
Status: util.Iif(len(needs) > 0, actions_model.StatusPending, actions_model.StatusBlocked),
|
||||
ParentJobID: caller.ID,
|
||||
WorkflowSourceRepoID: sourceRepoID,
|
||||
WorkflowSourceCommitSHA: sourceCommitSHA,
|
||||
|
||||
@@ -191,7 +191,10 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
||||
payload, _ := workflowJob.Marshal()
|
||||
|
||||
isReusableWorkflowCaller := job.Uses != ""
|
||||
shouldBlockJob := runAttempt.Status == actions_model.StatusBlocked || len(needs) > 0 || run.NeedApproval
|
||||
status := util.Iif(runAttempt.Status == actions_model.StatusBlocked || run.NeedApproval, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
||||
if status.IsWaiting() && len(needs) > 0 {
|
||||
status = actions_model.StatusPending
|
||||
}
|
||||
|
||||
attemptJobID, err := actions_model.GetNextAttemptJobID(ctx, run.ID)
|
||||
if err != nil {
|
||||
@@ -213,7 +216,7 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
||||
AttemptJobID: attemptJobID,
|
||||
Needs: needs,
|
||||
RunsOn: job.RunsOn(),
|
||||
Status: util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting),
|
||||
Status: status,
|
||||
WorkflowSourceRepoID: run.WorkflowRepoID,
|
||||
WorkflowSourceCommitSHA: run.WorkflowCommitSHA,
|
||||
ContinueOnError: job.GetContinueOnError(),
|
||||
@@ -256,9 +259,7 @@ func insertRunJob(ctx context.Context, run *actions_model.ActionRun, runAttempt
|
||||
}
|
||||
}
|
||||
|
||||
// If a job needs other jobs ("needs" is not empty), its status is set to StatusBlocked at the entry of the loop
|
||||
// No need to check job concurrency for a blocked job (it will be checked by job emitter later)
|
||||
// A slot-starved job skips the check too: it will not start, so it must not cancel its group peers.
|
||||
// A slot-starved job skips the check: it will not start, so it must not cancel its group peers.
|
||||
if runJob.Status == actions_model.StatusWaiting && slots.available(runJob) {
|
||||
var jobsToCancel []*actions_model.ActionRunJob
|
||||
runJob.Status, jobsToCancel, err = PrepareToStartJobWithConcurrency(ctx, runJob)
|
||||
|
||||
@@ -178,10 +178,19 @@ func TestToActionWorkflowJob_StepStatusIsIndependentOfJobStatus(t *testing.T) {
|
||||
assert.Equal(t, "failure", apiJob.Steps[1].Conclusion, "step 1 conclusion")
|
||||
}
|
||||
|
||||
func TestToActionsStatus_Cancelling(t *testing.T) {
|
||||
action, conclusion := ToActionsStatus(actions_model.StatusCancelling)
|
||||
assert.Equal(t, "in_progress", action)
|
||||
assert.Empty(t, conclusion)
|
||||
func TestToActionsStatus(t *testing.T) {
|
||||
for status, expected := range map[actions_model.Status]string{
|
||||
actions_model.StatusWaiting: "queued",
|
||||
actions_model.StatusBlocked: "pending",
|
||||
actions_model.StatusPending: "requested",
|
||||
actions_model.StatusCancelling: "in_progress",
|
||||
} {
|
||||
action, conclusion := ToActionsStatus(status)
|
||||
assert.Equal(t, expected, action, status.String())
|
||||
assert.Empty(t, conclusion)
|
||||
}
|
||||
action, _ := ToRunActionsStatus(&actions_model.ActionRun{NeedApproval: true}, actions_model.StatusBlocked)
|
||||
assert.Equal(t, "waiting", action)
|
||||
}
|
||||
|
||||
func TestToWorkflowRunAction_Cancelling(t *testing.T) {
|
||||
|
||||
@@ -273,7 +273,7 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
||||
}
|
||||
|
||||
runAttempt := int64(0)
|
||||
status, conclusion := ToActionsStatus(run.Status)
|
||||
status, conclusion := ToRunActionsStatus(run, run.Status)
|
||||
startedAt := run.Started.AsLocalTime()
|
||||
completedAt := run.Stopped.AsLocalTime()
|
||||
actor := run.TriggerUser // The username of the user that triggered the initial workflow run.
|
||||
@@ -289,7 +289,7 @@ func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, atte
|
||||
return nil, err
|
||||
}
|
||||
runAttempt = attempt.Attempt
|
||||
status, conclusion = ToActionsStatus(attempt.Status)
|
||||
status, conclusion = ToRunActionsStatus(run, attempt.Status)
|
||||
startedAt = attempt.Started.AsLocalTime()
|
||||
completedAt = attempt.Stopped.AsLocalTime()
|
||||
triggerUser = attempt.TriggerUser
|
||||
@@ -435,12 +435,21 @@ func ToWorkflowRunAction(status actions_model.Status) (action string) {
|
||||
return action
|
||||
}
|
||||
|
||||
func ToRunActionsStatus(run *actions_model.ActionRun, status actions_model.Status) (action, conclusion string) {
|
||||
if status.IsBlocked() && run.NeedApproval {
|
||||
return "waiting", ""
|
||||
}
|
||||
return ToActionsStatus(status)
|
||||
}
|
||||
|
||||
func ToActionsStatus(status actions_model.Status) (action, conclusion string) {
|
||||
switch status {
|
||||
case actions_model.StatusWaiting:
|
||||
action = "queued" // "waiting" is a naming conflict of the webhook between Gitea and GitHub Actions
|
||||
action = "queued"
|
||||
case actions_model.StatusBlocked:
|
||||
action = "waiting" // naming conflict (as above)
|
||||
action = "pending"
|
||||
case actions_model.StatusPending:
|
||||
action = "requested"
|
||||
case actions_model.StatusRunning, actions_model.StatusCancelling:
|
||||
action = "in_progress"
|
||||
default:
|
||||
@@ -469,7 +478,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status, conclusion := ToActionsStatus(job.Status)
|
||||
status, conclusion := ToRunActionsStatus(job.Run, job.Status)
|
||||
var runnerID int64
|
||||
var runnerName string
|
||||
var steps []*api.ActionWorkflowStep
|
||||
|
||||
@@ -343,7 +343,7 @@ func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkForm
|
||||
|
||||
text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink)
|
||||
switch description {
|
||||
case "waiting":
|
||||
case "waiting", "pending":
|
||||
color = orangeColor
|
||||
case "queued":
|
||||
color = orangeColorLight
|
||||
|
||||
@@ -998,6 +998,14 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo
|
||||
}
|
||||
|
||||
func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
log.Error("LoadRun: %v", err)
|
||||
return
|
||||
}
|
||||
status, _ := convert.ToRunActionsStatus(job.Run, job.Status)
|
||||
if status == "requested" || status == "pending" {
|
||||
return // announce a job only once it is queued
|
||||
}
|
||||
source := EventSource{
|
||||
Repository: repo,
|
||||
Owner: repo.Owner,
|
||||
@@ -1008,8 +1016,6 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
|
||||
org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
|
||||
}
|
||||
|
||||
status, _ := convert.ToActionsStatus(job.Status)
|
||||
|
||||
convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job)
|
||||
if err != nil {
|
||||
log.Error("ToActionWorkflowJob: %v", err)
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
data-locale-status-cancelled="{{ctx.Locale.Tr "actions.status.cancelled"}}"
|
||||
data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}"
|
||||
data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}"
|
||||
data-locale-status-pending="{{ctx.Locale.Tr "actions.status.pending"}}"
|
||||
data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}"
|
||||
data-locale-artifact-expired="{{ctx.Locale.Tr "expired"}}"
|
||||
data-locale-artifact-expires-at="{{ctx.Locale.Tr "artifact_expires_at"}}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{/* Status icons used for runs, jobs and steps.
|
||||
|
||||
Template Attributes:
|
||||
* Status: one of success, skipped, waiting, blocked, running, failure, cancelled, cancelling, unknown
|
||||
* Status: one of success, skipped, waiting, pending, blocked, running, failure, cancelled, cancelling, unknown
|
||||
* Size: icon size in pixels (default 16)
|
||||
* ClassName: additional CSS classes
|
||||
* IconVariant: "circle-fill" → octicon-check-circle-fill / octicon-x-circle-fill
|
||||
@@ -18,6 +18,8 @@ Keep this template in sync with web_src/js/components/ActionStatusIcon.vue.
|
||||
{{else if eq .Status "cancelled"}}
|
||||
{{svg "octicon-stop" $size (printf "tw-text-text-light %s" $className)}}
|
||||
{{else if eq .Status "waiting"}}
|
||||
{{svg "octicon-dot-fill" $size (printf "tw-text-yellow %s" $className)}}
|
||||
{{else if eq .Status "pending"}}
|
||||
{{svg "octicon-circle" $size (printf "tw-text-text-light %s" $className)}}
|
||||
{{else if eq .Status "blocked"}}
|
||||
{{svg "octicon-blocked" $size (printf "tw-text-yellow %s" $className)}}
|
||||
|
||||
+11
-11
@@ -11128,7 +11128,7 @@
|
||||
"operationId": "listAdminWorkflowJobs",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -11355,7 +11355,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -13141,7 +13141,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -13405,7 +13405,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -17183,7 +17183,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -17618,7 +17618,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -17990,7 +17990,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -18174,7 +18174,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -19321,7 +19321,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -34138,7 +34138,7 @@
|
||||
"operationId": "getUserWorkflowJobs",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
@@ -34365,7 +34365,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
|
||||
+11
-11
@@ -34,7 +34,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -257,7 +257,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -2040,7 +2040,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -2292,7 +2292,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -5784,7 +5784,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -6185,7 +6185,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -6521,7 +6521,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -6690,7 +6690,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -7747,7 +7747,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -21414,7 +21414,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -21637,7 +21637,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
|
||||
"description": "workflow status (requested, pending, queued, in_progress, failure, success, skipped)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
|
||||
@@ -603,6 +603,11 @@ jobs:
|
||||
})
|
||||
// cannot fetch wf2-job2 because wf1-job1 is running
|
||||
runner1.fetchNoTask(t)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs?status=pending", user2.Name, repo.Name)).AddTokenAuth(token)
|
||||
pendingJobs := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.ActionWorkflowJobsResponse{})
|
||||
require.Len(t, pendingJobs.Entries, 1)
|
||||
assert.Equal(t, "wf2-job2", pendingJobs.Entries[0].Name)
|
||||
assert.Equal(t, "pending", pendingJobs.Entries[0].Status)
|
||||
// exec wf1-job1
|
||||
runner1.execTask(t, wf1Job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
|
||||
@@ -411,7 +411,7 @@ jobs:
|
||||
require.Contains(t, rerunJobsByJobID, "job1")
|
||||
require.Contains(t, rerunJobsByJobID, "job2")
|
||||
assert.Equal(t, actions_model.StatusWaiting, rerunJobsByJobID["job1"].Status)
|
||||
assert.Equal(t, actions_model.StatusBlocked, rerunJobsByJobID["job2"].Status)
|
||||
assert.Equal(t, actions_model.StatusPending, rerunJobsByJobID["job2"].Status)
|
||||
|
||||
// fetch job1 rerun task
|
||||
job1TaskR1 := runner.fetchTask(t)
|
||||
|
||||
@@ -184,13 +184,13 @@ jobs:
|
||||
callerJob2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "caller_job2"})
|
||||
callerJob2ID = callerJob2.ID
|
||||
callerJob2AttemptJobID = callerJob2.AttemptJobID
|
||||
assert.Equal(t, actions_model.StatusBlocked, callerJob2.Status)
|
||||
assert.Equal(t, actions_model.StatusPending, callerJob2.Status)
|
||||
assert.True(t, callerJob2.IsReusableCaller)
|
||||
|
||||
// caller_job3
|
||||
callerJob3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "caller_job3"})
|
||||
callerJob3AttemptJobID = callerJob3.AttemptJobID
|
||||
assert.Equal(t, actions_model.StatusBlocked, callerJob3.Status)
|
||||
assert.Equal(t, actions_model.StatusPending, callerJob3.Status)
|
||||
assert.False(t, callerJob3.IsReusableCaller)
|
||||
})
|
||||
|
||||
@@ -230,9 +230,8 @@ jobs:
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// reusable1_job3 (a nested caller) needs reusable1_job2, so it stays Blocked until r1j2 succeeds.
|
||||
r1Job3Pre := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "reusable1_job3"})
|
||||
assert.Equal(t, actions_model.StatusBlocked, r1Job3Pre.Status)
|
||||
assert.Equal(t, actions_model.StatusPending, r1Job3Pre.Status)
|
||||
assert.False(t, r1Job3Pre.IsExpanded)
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: runID, JobID: "reusable2_job1"}))
|
||||
|
||||
@@ -308,11 +307,11 @@ jobs:
|
||||
callerJob2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: callerJob2AttemptJobID})
|
||||
assert.Equal(t, actions_model.StatusWaiting, callerJob2.Status)
|
||||
callerJob3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: callerJob3AttemptJobID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, callerJob3.Status)
|
||||
assert.Equal(t, actions_model.StatusPending, callerJob3.Status)
|
||||
|
||||
// reusable1_job3 needs reusable1_job2, so rerunning r1j2 pulls r1j3 (and its subtree) into the rerun set
|
||||
r1Job3Attempt2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: r1Job3AttemptJobID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, r1Job3Attempt2.Status)
|
||||
assert.Equal(t, actions_model.StatusPending, r1Job3Attempt2.Status)
|
||||
assert.True(t, r1Job3Attempt2.IsReusableCaller)
|
||||
assert.False(t, r1Job3Attempt2.IsExpanded)
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, JobID: "reusable2_job1"}))
|
||||
@@ -606,12 +605,11 @@ jobs:
|
||||
uses: ./.gitea/workflows/does-not-exist.yml
|
||||
`)
|
||||
|
||||
// plain_job runs first; bad_caller is Blocked on needs and is NOT expanded at creation.
|
||||
plainTask := runner.fetchTask(t)
|
||||
_, plainJob, run := getTaskAndJobAndRunByTaskID(t, plainTask.Id)
|
||||
assert.Equal(t, "plain_job", plainJob.JobID)
|
||||
badCallerPre := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "bad_caller"})
|
||||
assert.Equal(t, actions_model.StatusBlocked, badCallerPre.Status)
|
||||
assert.Equal(t, actions_model.StatusPending, badCallerPre.Status)
|
||||
assert.False(t, badCallerPre.IsExpanded)
|
||||
|
||||
runner.execTask(t, plainTask, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
@@ -975,7 +973,6 @@ jobs:
|
||||
innerChild := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "inner"})
|
||||
assert.Equal(t, willRun.ID, innerChild.ParentJobID)
|
||||
|
||||
// after_skip: a dependent of the skipped caller resolves to Skipped instead of staying Blocked.
|
||||
afterSkip := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "after_skip"})
|
||||
assert.Equal(t, actions_model.StatusSkipped, afterSkip.Status)
|
||||
})
|
||||
|
||||
@@ -1045,7 +1045,7 @@ jobs:
|
||||
|
||||
// 3. validate the webhook is triggered
|
||||
assert.Equal(t, "workflow_job", triggeredEvent)
|
||||
assert.Len(t, payloads, 2)
|
||||
assert.Len(t, payloads, 1)
|
||||
assert.Equal(t, "queued", payloads[0].Action)
|
||||
assert.Equal(t, "queued", payloads[0].WorkflowJob.Status)
|
||||
assert.Equal(t, []string{"ubuntu-latest"}, payloads[0].WorkflowJob.Labels)
|
||||
@@ -1053,11 +1053,11 @@ jobs:
|
||||
assert.Equal(t, "repo1", payloads[0].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[0].Repo.FullName)
|
||||
|
||||
assert.Equal(t, "waiting", payloads[1].Action)
|
||||
assert.Equal(t, "waiting", payloads[1].WorkflowJob.Status)
|
||||
assert.Equal(t, commitID, payloads[1].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[1].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[1].Repo.FullName)
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/actions/jobs?status=requested").AddTokenAuth(token)
|
||||
requestedJobs := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.ActionWorkflowJobsResponse{})
|
||||
require.Len(t, requestedJobs.Entries, 1)
|
||||
assert.Equal(t, "wf2-job", requestedJobs.Entries[0].Name)
|
||||
assert.Equal(t, "requested", requestedJobs.Entries[0].Status)
|
||||
|
||||
// 4. Execute a single Job
|
||||
task := runner.fetchTask(t)
|
||||
@@ -1068,31 +1068,31 @@ jobs:
|
||||
|
||||
// 5. validate the webhook is triggered
|
||||
assert.Equal(t, "workflow_job", triggeredEvent)
|
||||
assert.Len(t, payloads, 5)
|
||||
assert.Equal(t, "in_progress", payloads[2].Action)
|
||||
assert.Equal(t, "in_progress", payloads[2].WorkflowJob.Status)
|
||||
assert.Len(t, payloads, 4)
|
||||
assert.Equal(t, "in_progress", payloads[1].Action)
|
||||
assert.Equal(t, "in_progress", payloads[1].WorkflowJob.Status)
|
||||
assert.Equal(t, "mock-runner", payloads[1].WorkflowJob.RunnerName)
|
||||
assert.Equal(t, commitID, payloads[1].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[1].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[1].Repo.FullName)
|
||||
|
||||
assert.Equal(t, "completed", payloads[2].Action)
|
||||
assert.Equal(t, "completed", payloads[2].WorkflowJob.Status)
|
||||
assert.Equal(t, "mock-runner", payloads[2].WorkflowJob.RunnerName)
|
||||
assert.Equal(t, "success", payloads[2].WorkflowJob.Conclusion)
|
||||
assert.Equal(t, commitID, payloads[2].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[2].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[2].Repo.FullName)
|
||||
assert.Contains(t, payloads[2].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[2].WorkflowJob.ID))
|
||||
assert.Contains(t, payloads[2].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", payloads[2].WorkflowJob.ID))
|
||||
assert.Len(t, payloads[2].WorkflowJob.Steps, 1)
|
||||
|
||||
assert.Equal(t, "completed", payloads[3].Action)
|
||||
assert.Equal(t, "completed", payloads[3].WorkflowJob.Status)
|
||||
assert.Equal(t, "mock-runner", payloads[3].WorkflowJob.RunnerName)
|
||||
assert.Equal(t, "success", payloads[3].WorkflowJob.Conclusion)
|
||||
assert.Equal(t, "queued", payloads[3].Action)
|
||||
assert.Equal(t, "queued", payloads[3].WorkflowJob.Status)
|
||||
assert.Equal(t, []string{"ubuntu-latest"}, payloads[3].WorkflowJob.Labels)
|
||||
assert.Equal(t, commitID, payloads[3].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[3].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[3].Repo.FullName)
|
||||
assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID))
|
||||
assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", payloads[3].WorkflowJob.ID))
|
||||
assert.Len(t, payloads[3].WorkflowJob.Steps, 1)
|
||||
|
||||
assert.Equal(t, "queued", payloads[4].Action)
|
||||
assert.Equal(t, "queued", payloads[4].WorkflowJob.Status)
|
||||
assert.Equal(t, []string{"ubuntu-latest"}, payloads[4].WorkflowJob.Labels)
|
||||
assert.Equal(t, commitID, payloads[4].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[4].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[4].Repo.FullName)
|
||||
|
||||
// 6. Execute a single Job
|
||||
task = runner.fetchTask(t)
|
||||
@@ -1103,25 +1103,25 @@ jobs:
|
||||
|
||||
// 7. validate the webhook is triggered
|
||||
assert.Equal(t, "workflow_job", triggeredEvent)
|
||||
assert.Len(t, payloads, 7)
|
||||
assert.Equal(t, "in_progress", payloads[5].Action)
|
||||
assert.Equal(t, "in_progress", payloads[5].WorkflowJob.Status)
|
||||
assert.Equal(t, "mock-runner", payloads[5].WorkflowJob.RunnerName)
|
||||
assert.Len(t, payloads, 6)
|
||||
assert.Equal(t, "in_progress", payloads[4].Action)
|
||||
assert.Equal(t, "in_progress", payloads[4].WorkflowJob.Status)
|
||||
assert.Equal(t, "mock-runner", payloads[4].WorkflowJob.RunnerName)
|
||||
|
||||
assert.Equal(t, commitID, payloads[4].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[4].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[4].Repo.FullName)
|
||||
|
||||
assert.Equal(t, "completed", payloads[5].Action)
|
||||
assert.Equal(t, "completed", payloads[5].WorkflowJob.Status)
|
||||
assert.Equal(t, "failure", payloads[5].WorkflowJob.Conclusion)
|
||||
assert.Equal(t, "mock-runner", payloads[5].WorkflowJob.RunnerName)
|
||||
assert.Equal(t, commitID, payloads[5].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[5].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[5].Repo.FullName)
|
||||
|
||||
assert.Equal(t, "completed", payloads[6].Action)
|
||||
assert.Equal(t, "completed", payloads[6].WorkflowJob.Status)
|
||||
assert.Equal(t, "failure", payloads[6].WorkflowJob.Conclusion)
|
||||
assert.Equal(t, "mock-runner", payloads[6].WorkflowJob.RunnerName)
|
||||
assert.Equal(t, commitID, payloads[6].WorkflowJob.HeadSha)
|
||||
assert.Equal(t, "repo1", payloads[6].Repo.Name)
|
||||
assert.Equal(t, "user2/repo1", payloads[6].Repo.FullName)
|
||||
assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID))
|
||||
assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", payloads[6].WorkflowJob.ID))
|
||||
assert.Len(t, payloads[6].WorkflowJob.Steps, 2)
|
||||
assert.Contains(t, payloads[5].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[5].WorkflowJob.ID))
|
||||
assert.Contains(t, payloads[5].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", payloads[5].WorkflowJob.ID))
|
||||
assert.Len(t, payloads[5].WorkflowJob.Steps, 2)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<!-- Keep in sync with templates/repo/icons/action_status.tmpl.
|
||||
action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, cancelling, unknown.
|
||||
-->
|
||||
<!-- Keep in sync with templates/repo/icons/action_status.tmpl. -->
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue';
|
||||
import SvgIcon from './SvgIcon.vue';
|
||||
import {getActionStatusIcon, type ActionStatusIconVariant} from '../modules/action-status-icon.ts';
|
||||
import type {ActionsStatus} from '../modules/gitea-actions.ts';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
status: 'success' | 'skipped' | 'waiting' | 'blocked' | 'running' | 'failure' | 'cancelled' | 'cancelling' | 'unknown',
|
||||
status: ActionsStatus,
|
||||
size?: number,
|
||||
className?: string,
|
||||
localeStatus?: string,
|
||||
|
||||
@@ -92,7 +92,7 @@ function isJobCollapsed(jobID: number) {
|
||||
}
|
||||
|
||||
const visibleJobListItems = computed<JobListItem[]>(() => {
|
||||
const jobs = [...(run.value.jobs || [])].sort((a, b) => a.id - b.id);
|
||||
const jobs = (run.value.jobs || []).filter((job) => job.status !== 'pending' || job.id === props.jobId).sort((a, b) => a.id - b.id);
|
||||
const childrenByParent = buildJobsByParentJobID(jobs);
|
||||
|
||||
const result: JobListItem[] = [];
|
||||
|
||||
@@ -160,7 +160,7 @@ test('reusable callers with identical dependency signature are kept as separate
|
||||
{id: 1, link: '', jobId: 'prepare', name: 'prepare', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '30s'},
|
||||
{id: 2, link: '', jobId: 'local_caller', name: 'local caller', status: 'running', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '5m', needs: ['prepare'], callUses: './.gitea/workflows/lib.yml'},
|
||||
{id: 3, link: '', jobId: 'cross_caller', name: 'cross-repo caller', status: 'waiting', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '0s', needs: ['prepare'], callUses: 'user2/lib/.gitea/workflows/ext.yml@main'},
|
||||
{id: 4, link: '', jobId: 'final', name: 'final', status: 'blocked', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']},
|
||||
{id: 4, link: '', jobId: 'final', name: 'final', status: 'pending', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']},
|
||||
];
|
||||
const graph = createWorkflowGraphModel(jobs);
|
||||
expect(graph.nodes.find((n) => n.type === 'group')).toBeUndefined();
|
||||
|
||||
@@ -122,9 +122,10 @@ function compareStatusWorstFirst(a: ActionsStatus, b: ActionsStatus): number {
|
||||
if (s === 'running') return 2;
|
||||
if (s === 'waiting') return 3;
|
||||
if (s === 'blocked') return 4;
|
||||
if (s === 'success') return 5;
|
||||
if (s === 'skipped') return 6;
|
||||
return 7;
|
||||
if (s === 'pending') return 5;
|
||||
if (s === 'success') return 6;
|
||||
if (s === 'skipped') return 7;
|
||||
return 8;
|
||||
};
|
||||
return rank(a) - rank(b);
|
||||
}
|
||||
|
||||
@@ -228,11 +228,13 @@ const splitRoutedEdges = computed(() => {
|
||||
const nodesWithIncomingEdge = computed(() => new Set(graphModel.value.adjacency.incomingByNodeId.keys()));
|
||||
const nodesWithOutgoingEdge = computed(() => new Set(graphModel.value.adjacency.outgoingByNodeId.keys()));
|
||||
|
||||
function isJobLinked(job: ActionsJob) {
|
||||
return !job.isReusableCaller && job.status !== 'pending'; // callers have no detail page, pending jobs have nothing to show yet
|
||||
}
|
||||
|
||||
function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
const target = 'jobs' in job ? job.jobs[0]! : job;
|
||||
// Reusable callers have no per-job detail page; clicking them is a no-op so the graph
|
||||
// doesn't lead users to a dead destination.
|
||||
if (target.isReusableCaller) return;
|
||||
if (!isJobLinked(target)) return;
|
||||
const link = `${props.runLink}/jobs/${target.id}`;
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(link, '_blank');
|
||||
@@ -342,6 +344,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
v-for="ch in job.jobs"
|
||||
:key="ch.id"
|
||||
class="graph-list-row"
|
||||
:class="{ 'unlinked-node': !isJobLinked(ch) }"
|
||||
@mouseenter="handleNodeMouseEnter(job.id)"
|
||||
@click.stop="onNodeClick(ch, $event)"
|
||||
>
|
||||
@@ -373,6 +376,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
v-for="ch in job.jobs"
|
||||
:key="ch.id"
|
||||
class="graph-list-row"
|
||||
:class="{ 'unlinked-node': !isJobLinked(ch) }"
|
||||
@mouseenter="handleNodeMouseEnter(job.id)"
|
||||
@click="onNodeClick(ch, $event)"
|
||||
>
|
||||
@@ -391,7 +395,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
<g
|
||||
v-else
|
||||
class="job-node-group"
|
||||
:class="{ 'related-node': isNodeHighlighted(job.id), 'caller-node': job.jobs[0]!.isReusableCaller }"
|
||||
:class="{ 'related-node': isNodeHighlighted(job.id), 'unlinked-node': !isJobLinked(job.jobs[0]!) }"
|
||||
@click="onNodeClick(job, $event)"
|
||||
@mouseenter="handleNodeMouseEnter(job.id)"
|
||||
@mouseleave="handleNodeMouseLeave"
|
||||
@@ -523,10 +527,15 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.job-node-group.caller-node {
|
||||
.job-node-group.unlinked-node,
|
||||
.graph-list-row.unlinked-node {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.graph-list-row.unlinked-node:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.job-node-group:hover .job-rect,
|
||||
.job-node-group.related-node .job-rect {
|
||||
stroke: var(--color-primary);
|
||||
|
||||
@@ -89,6 +89,7 @@ function initRepositoryActionsView() {
|
||||
cancelled: el.getAttribute('data-locale-status-cancelled'),
|
||||
skipped: el.getAttribute('data-locale-status-skipped'),
|
||||
blocked: el.getAttribute('data-locale-status-blocked'),
|
||||
pending: el.getAttribute('data-locale-status-pending'),
|
||||
},
|
||||
logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'),
|
||||
logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
|
||||
|
||||
@@ -19,6 +19,8 @@ export function getActionStatusIcon(status: ActionsStatus, iconVariant: ActionSt
|
||||
case 'cancelled':
|
||||
return {name: 'octicon-stop', colorClass: 'tw-text-text-light'};
|
||||
case 'waiting':
|
||||
return {name: 'octicon-dot-fill', colorClass: 'tw-text-yellow'};
|
||||
case 'pending':
|
||||
return {name: 'octicon-circle', colorClass: 'tw-text-text-light'};
|
||||
case 'blocked':
|
||||
return {name: 'octicon-blocked', colorClass: 'tw-text-yellow'};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
|
||||
export type ActionsStatus = 'unknown' | 'waiting' | 'running' | 'cancelling' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
|
||||
export type ActionsStatus = 'unknown' | 'waiting' | 'running' | 'cancelling' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked' | 'pending';
|
||||
export type ActionsArtifactStatus = 'expired' | 'completed';
|
||||
|
||||
export type ActionsRun = {
|
||||
|
||||
Reference in New Issue
Block a user