diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 6c24f86cae0..94873ea125f 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -165,11 +165,12 @@ func ScopedWorkflowStatusContextName(prefix, displayName, jobName, event string) return strings.TrimSpace(fmt.Sprintf("%s: %s", prefix, WorkflowStatusContextName(displayName, jobName, event))) } -// ShouldEventCreateCommitStatus reports whether a run triggered by the given workflow `on:` event posts a commit status, +// IsPullRequestCheckEvent reports whether a run triggered by the given workflow `on:` event posts a commit status that pull requests count, // so its context can serve as a required status check. -// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted. -// The two are kept in sync by hand and can drift; unify them into a single source so adding a status-producing event in one place automatically updates the other. -func ShouldEventCreateCommitStatus(event string) bool { +// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted, +// and in services/pull.GetLatestPullCommitStatuses, which decides whether pull requests count it. +// They are kept in sync by hand and can drift, unify them into a single source so adding a status-producing event in one place automatically updates the others. +func IsPullRequestCheckEvent(event string) bool { switch event { case "push", "pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment", "release": return true diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 18a4086710c..944b985e912 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -37,9 +37,10 @@ const ( // FIXME: This event should be a group of pull_request_review_xxx events HookEventPullRequestReview HookEventType = "pull_request_review" // Actions event only - HookEventSchedule HookEventType = "schedule" - HookEventWorkflowRun HookEventType = "workflow_run" - HookEventWorkflowJob HookEventType = "workflow_job" + HookEventSchedule HookEventType = "schedule" + HookEventWorkflowDispatch HookEventType = "workflow_dispatch" + HookEventWorkflowRun HookEventType = "workflow_run" + HookEventWorkflowJob HookEventType = "workflow_job" ) func AllEvents() []HookEventType { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index cdb47fa3fd4..22dacfd24f1 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -420,9 +420,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.C statusCheckData := &pullCommitStatusCheckData{} data.StatusCheckData = statusCheckData - commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID, db.ListOptionsAll) + commitStatuses, err := pull_service.GetLatestPullCommitStatuses(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID) if err != nil { - log.Error("GetLatestCommitStatus: %v", err) + log.Error("GetLatestPullCommitStatuses: %v", err) } // Effective required contexts = branch-protection contexts + required scoped workflow checks. diff --git a/routers/web/shared/actions/scoped_workflows.go b/routers/web/shared/actions/scoped_workflows.go index 0b5a90901f8..c1af3c26b05 100644 --- a/routers/web/shared/actions/scoped_workflows.go +++ b/routers/web/shared/actions/scoped_workflows.go @@ -158,8 +158,7 @@ func deriveScopedStatusContexts(prefix, displayName string, content []byte, even } eventNames := make([]string, 0, len(events)) for _, e := range events { - // only events whose runs post a commit status can be a required check; workflow_dispatch, schedule, etc. post none. - if actions_module.ShouldEventCreateCommitStatus(e.Name) { + if actions_module.IsPullRequestCheckEvent(e.Name) { eventNames = append(eventNames, e.Name) } } diff --git a/routers/web/shared/actions/scoped_workflows_test.go b/routers/web/shared/actions/scoped_workflows_test.go index a51861bdc68..c320bbe48ff 100644 --- a/routers/web/shared/actions/scoped_workflows_test.go +++ b/routers/web/shared/actions/scoped_workflows_test.go @@ -48,7 +48,7 @@ jobs: }, got) }) - t.Run("only status-producing events; workflow_dispatch/schedule/workflow_call skipped", func(t *testing.T) { + t.Run("only pull request check events; workflow_dispatch/schedule/workflow_call skipped", func(t *testing.T) { content := []byte(`name: CI on: push: @@ -65,21 +65,6 @@ jobs: events, err := actions_module.GetEventsFromContent(content) require.NoError(t, err) got := deriveScopedStatusContexts("org/src", "CI", content, events) - assert.Equal(t, []string{"org/src: CI / j (push)"}, got) // only push posts a commit status - }) - - t.Run("a workflow_dispatch-only workflow has no expected contexts", func(t *testing.T) { - content := []byte(`name: Deploy -on: workflow_dispatch -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - run: echo -`) - events, err := actions_module.GetEventsFromContent(content) - require.NoError(t, err) - got := deriveScopedStatusContexts("org/src", "Deploy", content, events) - assert.Empty(t, got) // workflow_dispatch posts no commit status -> nothing to preview (and it cannot be a required check) + assert.Equal(t, []string{"org/src: CI / j (push)"}, got) }) } diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 08df31f575c..1b98c514b00 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -121,7 +121,7 @@ func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, c webhook_module.HookEventPullRequestReviewComment: event = run.TriggerEvent commitID = run.CommitSHA - case webhook_module.HookEventRelease: + case webhook_module.HookEventRelease, webhook_module.HookEventWorkflowDispatch: event = string(run.Event) commitID = run.CommitSHA default: // do nothing, return empty diff --git a/services/actions/concurrency.go b/services/actions/concurrency.go index ba66a84433f..7f4dce194ea 100644 --- a/services/actions/concurrency.go +++ b/services/actions/concurrency.go @@ -12,6 +12,7 @@ import ( "gitea.dev/modules/actions/jobparser" "gitea.dev/modules/setting" "gitea.dev/modules/util" + webhook_module "gitea.dev/modules/webhook" "go.yaml.in/yaml/v4" ) @@ -29,7 +30,7 @@ func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.Act actionsRunCtx := GenerateGiteaContext(ctx, run, attempt, nil) jobResults := map[string]*jobparser.JobResult{"": {}} if inputs == nil { - if run.Event == "workflow_dispatch" { + if run.Event == webhook_module.HookEventWorkflowDispatch { setting.PanicInDevOrTesting("run %d: workflow_dispatch inputs must be resolved by the caller", run.ID) } inputs = map[string]any{} diff --git a/services/actions/helper.go b/services/actions/helper.go index 75602adb45d..60b9a71fd91 100644 --- a/services/actions/helper.go +++ b/services/actions/helper.go @@ -17,11 +17,12 @@ import ( "gitea.dev/modules/log" api "gitea.dev/modules/structs" "gitea.dev/modules/util" + webhook_module "gitea.dev/modules/webhook" ) // dispatchInputsForJob types a top-level job's `inputs.*` from EventPayload, empty for other events. func dispatchInputsForJob(run *actions_model.ActionRun, job *actions_model.ActionRunJob) (map[string]any, error) { - if run.Event != "workflow_dispatch" { + if run.Event != webhook_module.HookEventWorkflowDispatch { return map[string]any{}, nil } var payload api.WorkflowDispatchPayload @@ -85,7 +86,7 @@ func decodeWorkflowCallInputs(caller *actions_model.ActionRunJob) (map[string]an // calledWorkflowInputs overlays the run's dispatch inputs with `workflowCallInputs`, as GitHub does. func calledWorkflowInputs(ctx context.Context, run *actions_model.ActionRun, caller *actions_model.ActionRunJob, workflowCallInputs map[string]any) (map[string]any, error) { top := caller - for run.Event == "workflow_dispatch" && top.ParentJobID != 0 { + for run.Event == webhook_module.HookEventWorkflowDispatch && top.ParentJobID != 0 { parent, err := actions_model.GetRunJobByRunAndID(ctx, run.ID, top.ParentJobID) if err != nil { return nil, fmt.Errorf("load caller job %d: %w", top.ParentJobID, err) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 8a82c158a50..32d9e559f4c 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -22,6 +22,7 @@ import ( "gitea.dev/modules/reqctx" api "gitea.dev/modules/structs" "gitea.dev/modules/util" + webhook_module "gitea.dev/modules/webhook" "gitea.dev/services/audit" gitea_context "gitea.dev/services/context" "gitea.dev/services/convert" @@ -129,7 +130,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re Ref: string(refName), CommitSHA: runTargetCommit.ID.String(), IsForkPullRequest: false, - Event: "workflow_dispatch", + Event: webhook_module.HookEventWorkflowDispatch, TriggerEvent: "workflow_dispatch", Status: actions_model.StatusWaiting, // local dispatch: own repo at the target commit; the scoped path overrides these below diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index a48ce2287c7..b741988d1a6 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -9,12 +9,14 @@ import ( "errors" "fmt" "slices" + "strings" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" git_model "gitea.dev/models/git" issues_model "gitea.dev/models/issues" repo_model "gitea.dev/models/repo" + user_model "gitea.dev/models/user" "gitea.dev/modules/commitstatus" "gitea.dev/modules/container" "gitea.dev/modules/git" @@ -139,9 +141,9 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR return "", fmt.Errorf("LoadBaseRepo: %w", err) } - commitStatuses, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll) + commitStatuses, err := GetLatestPullCommitStatuses(ctx, pr.BaseRepo.ID, sha) if err != nil { - return "", fmt.Errorf("GetLatestCommitStatus: %w", err) + return "", fmt.Errorf("GetLatestPullCommitStatuses: %w", err) } pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) @@ -156,6 +158,14 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil } +// GetLatestPullCommitStatuses returns a pull request head commit's latest statuses without workflow_dispatch runs', which don't count for pull requests +func GetLatestPullCommitStatuses(ctx context.Context, repoID int64, sha string) ([]*git_model.CommitStatus, error) { + statuses, err := git_model.GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll) + return slices.DeleteFunc(statuses, func(status *git_model.CommitStatus) bool { + return status.CreatorID == user_model.ActionsUserID && strings.HasSuffix(status.Context, " (workflow_dispatch)") + }), err +} + // EffectiveRequiredContexts returns the required status-check contexts to enforce, drawn from: // 1. every required scoped workflow's status-check patterns effective for the repo (always) // 2. each given protected branch rule's own configured contexts, only when that rule's status check is enabled diff --git a/services/pull/pull.go b/services/pull/pull.go index 0a68e4dbd21..07c76e03597 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -937,7 +937,7 @@ func getAllCommitStatus(ctx context.Context, doer *user_model.User, gitRepo *git return nil, nil, shaErr } - statuses, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll) + statuses, err = GetLatestPullCommitStatuses(ctx, pr.BaseRepo.ID, sha) for _, status := range statuses { status.Repo = pr.BaseRepo // the repo is already loaded, spare the permission lookup a query } diff --git a/tests/integration/actions_inputs_test.go b/tests/integration/actions_inputs_test.go index 52f214a1c76..17b82ffedff 100644 --- a/tests/integration/actions_inputs_test.go +++ b/tests/integration/actions_inputs_test.go @@ -11,11 +11,15 @@ import ( actions_model "gitea.dev/models/actions" auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" + git_model "gitea.dev/models/git" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + pull_service "gitea.dev/services/pull" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWorkflowWithInputsContext(t *testing.T) { @@ -73,6 +77,15 @@ jobs: task := wRunner.fetchTask(t) _, _, run := getTaskAndJobAndRunByTaskID(t, task.Id) assert.Equal(t, "Build APP on windows", run.Title) + + statuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, run.CommitSHA, db.ListOptionsAll) + require.NoError(t, err) + require.Len(t, statuses, 1) + assert.Equal(t, "Test Inputs Context / build (workflow_dispatch)", statuses[0].Context) + + pullStatuses, err := pull_service.GetLatestPullCommitStatuses(t.Context(), repo.ID, run.CommitSHA) + require.NoError(t, err) + assert.Empty(t, pullStatuses) }) }