enhance(actions): add pending job status and align job statuses with GitHub (#39376)

This commit is contained in:
silverwind authored and GitHub committed 2026-09-27 18:30:42 +00:00
1 parent 0d09986790
commit 9e7b302bd3
45 files changed
+397 -189

No files matched your search

+49 -6
View File
@@ -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