From 678a97022ad4e53a41856d64fda942f86fcfc039 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 11 Oct 2025 00:48:06 +0200 Subject: [PATCH 1/7] Cleanup ActionRun creation * share more logic --- services/actions/notifier_helper.go | 60 ++-------------------------- services/actions/run.go | 59 +++++++++++++++++++++++++++ services/actions/schedule_tasks.go | 41 ++----------------- services/actions/workflow.go | 62 +---------------------------- 4 files changed, 67 insertions(+), 155 deletions(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index fc1894c5d889f..57f6aa6724771 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -29,7 +29,6 @@ import ( "code.gitea.io/gitea/services/convert" notify_service "code.gitea.io/gitea/services/notify" - "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" ) @@ -346,44 +345,8 @@ func handleWorkflows( run.NeedApproval = need - if err := run.LoadAttributes(ctx); err != nil { - log.Error("LoadAttributes: %v", err) - continue - } - - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - log.Error("GetVariablesOfRun: %v", err) - continue - } - - wfRawConcurrency, err := jobparser.ReadWorkflowRawConcurrency(dwf.Content) - if err != nil { - log.Error("ReadWorkflowRawConcurrency: %v", err) - continue - } - if wfRawConcurrency != nil { - err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars) - if err != nil { - log.Error("EvaluateRunConcurrencyFillModel: %v", err) - continue - } - } - - giteaCtx := GenerateGiteaContext(run, nil) - - jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext())) - if err != nil { - log.Error("jobparser.Parse: %v", err) - continue - } - - if len(jobs) > 0 && jobs[0].RunName != "" { - run.Title = jobs[0].RunName - } - - if err := InsertRun(ctx, run, jobs); err != nil { - log.Error("InsertRun: %v", err) + if err := PrepareRun(ctx, dwf.Content, run, nil); err != nil { + log.Error("PrepareRun: %v", err) continue } @@ -392,6 +355,7 @@ func handleWorkflows( log.Error("FindRunJobs: %v", err) continue } + // FIXME PERF skip this for schedule, dispatch etc. CreateCommitStatus(ctx, alljobs...) if len(alljobs) > 0 { job := alljobs[0] @@ -559,24 +523,6 @@ func handleSchedules( Content: dwf.Content, } - vars, err := actions_model.GetVariablesOfRun(ctx, run.ToActionRun()) - if err != nil { - log.Error("GetVariablesOfRun: %v", err) - continue - } - - giteaCtx := GenerateGiteaContext(run.ToActionRun(), nil) - - jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext())) - if err != nil { - log.Error("jobparser.Parse: %v", err) - continue - } - - if len(jobs) > 0 && jobs[0].RunName != "" { - run.Title = jobs[0].RunName - } - crons = append(crons, run) } diff --git a/services/actions/run.go b/services/actions/run.go index a3356d71c15d9..852daa2761b8a 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -9,12 +9,71 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/jobparser" "gopkg.in/yaml.v3" ) +// PrepareRun prepares a run before inserting it into the database +// It parses the workflow content, evaluates concurrency if needed, and inserts the run and its jobs into the database. +// The title will be cut off at 255 characters if it's longer than 255 characters. +func PrepareRun(ctx context.Context, content []byte, run *actions_model.ActionRun, inputsWithDefaults map[string]any) error { + if err := run.LoadAttributes(ctx); err != nil { + return fmt.Errorf("LoadAttributes: %w", err) + } + + vars, err := actions_model.GetVariablesOfRun(ctx, run) + if err != nil { + return fmt.Errorf("GetVariablesOfRun: %w", err) + } + + wfRawConcurrency, err := jobparser.ReadWorkflowRawConcurrency(content) + if err != nil { + return fmt.Errorf("ReadWorkflowRawConcurrency: %w", err) + } + + if wfRawConcurrency != nil { + err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars) + if err != nil { + return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err) + } + } + + giteaCtx := GenerateGiteaContext(run, nil) + + jobs, err := jobparser.Parse(content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()), jobparser.WithInputs(inputsWithDefaults)) + if err != nil { + return fmt.Errorf("parse workflow: %w", err) + } + + if len(jobs) > 0 && jobs[0].RunName != "" { + run.Title = jobs[0].RunName + } + + if err := InsertRun(ctx, run, jobs); err != nil { + return fmt.Errorf("InsertRun: %w", err) + } + + allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + log.Error("FindRunJobs: %v", err) + } + err = run.LoadAttributes(ctx) + if err != nil { + log.Error("LoadAttributes: %v", err) + } + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) + for _, job := range allJobs { + notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) + } + + // Return nil if no errors occurred + return nil +} + // InsertRun inserts a run // The title will be cut off at 255 characters if it's longer than 255 characters. func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow) error { diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 3b37b44ac4338..17e52a5865485 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -15,9 +15,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" - notify_service "code.gitea.io/gitea/services/notify" - - "github.com/nektos/act/pkg/jobparser" ) // StartScheduleTasks start the task @@ -119,44 +116,12 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) Status: actions_model.StatusWaiting, } - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - log.Error("GetVariablesOfRun: %v", err) - return err - } - - // Parse the workflow specification from the cron schedule - workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars)) - if err != nil { - return err - } - wfRawConcurrency, err := jobparser.ReadWorkflowRawConcurrency(cron.Content) - if err != nil { - return err - } - if wfRawConcurrency != nil { - err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars) - if err != nil { - return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err) - } - } - + // FIXME cron.Content might be outdated if the workflow file has been changed. + // Load the latest sha from default branch // Insert the action run and its associated jobs into the database - if err := InsertRun(ctx, run, workflows); err != nil { + if err := PrepareRun(ctx, cron.Content, run, nil); err != nil { return err } - allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - } - err = run.LoadAttributes(ctx) - if err != nil { - log.Error("LoadAttributes: %v", err) - } - notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) - for _, job := range allJobs { - notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) - } // Return nil if no errors occurred return nil diff --git a/services/actions/workflow.go b/services/actions/workflow.go index e3e60d496755b..1547342ff1fed 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -8,7 +8,6 @@ import ( "strings" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -16,13 +15,11 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/reqctx" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" @@ -98,9 +95,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } // find workflow from commit - var workflows []*jobparser.SingleWorkflow var entry *git.TreeEntry - var wfRawConcurrency *model.RawConcurrency run := &actions_model.ActionRun{ Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0], @@ -153,29 +148,6 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } } - giteaCtx := GenerateGiteaContext(run, nil) - - workflows, err = jobparser.Parse(content, jobparser.WithGitContext(giteaCtx.ToGitHubContext()), jobparser.WithInputs(inputsWithDefaults)) - if err != nil { - return err - } - - if len(workflows) > 0 && workflows[0].RunName != "" { - run.Title = workflows[0].RunName - } - - if len(workflows) == 0 { - return util.ErrorWrapLocale( - util.NewNotExistErrorf("workflow %q doesn't exist", workflowID), - "actions.workflow.not_found", workflowID, - ) - } - - wfRawConcurrency, err = jobparser.ReadWorkflowRawConcurrency(content) - if err != nil { - return err - } - // ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context // https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch @@ -193,39 +165,9 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } run.EventPayload = string(eventPayload) - // cancel running jobs of the same concurrency group - if wfRawConcurrency != nil { - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - return fmt.Errorf("GetVariablesOfRun: %w", err) - } - err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars) - if err != nil { - return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err) - } - } - // Insert the action run and its associated jobs into the database - if err := InsertRun(ctx, run, workflows); err != nil { - return fmt.Errorf("InsertRun: %w", err) - } - - allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - } - CreateCommitStatus(ctx, allJobs...) - if len(allJobs) > 0 { - job := allJobs[0] - err := job.LoadRun(ctx) - if err != nil { - log.Error("LoadRun: %v", err) - } else { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } - } - for _, job := range allJobs { - notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) + if err := PrepareRun(ctx, content, run, inputsWithDefaults); err != nil { + return fmt.Errorf("PrepareRun: %w", err) } return nil } From 0aec839b8a7b1b8b1d7d293c0ed69853b6775b6d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 11 Oct 2025 11:57:12 +0200 Subject: [PATCH 2/7] cleanup --- services/actions/notifier_helper.go | 21 --------------------- services/actions/run.go | 5 +++++ 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 57f6aa6724771..3f921b62c5856 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -27,7 +27,6 @@ import ( api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" - notify_service "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/model" ) @@ -349,26 +348,6 @@ func handleWorkflows( log.Error("PrepareRun: %v", err) continue } - - alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - log.Error("FindRunJobs: %v", err) - continue - } - // FIXME PERF skip this for schedule, dispatch etc. - CreateCommitStatus(ctx, alljobs...) - if len(alljobs) > 0 { - job := alljobs[0] - err := job.LoadRun(ctx) - if err != nil { - log.Error("LoadRun: %v", err) - continue - } - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } - for _, job := range alljobs { - notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil) - } } return nil } diff --git a/services/actions/run.go b/services/actions/run.go index 852daa2761b8a..39e63ad456578 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -57,10 +57,15 @@ func PrepareRun(ctx context.Context, content []byte, run *actions_model.ActionRu return fmt.Errorf("InsertRun: %w", err) } + // FIXME PERF do we need this db round trip? allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) if err != nil { log.Error("FindRunJobs: %v", err) } + + // FIXME PERF skip this for schedule, dispatch etc. + CreateCommitStatus(ctx, allJobs...) + err = run.LoadAttributes(ctx) if err != nil { log.Error("LoadAttributes: %v", err) From 948f65073c3073e7057c10d782337a24adfbfb0c Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sat, 11 Oct 2025 20:49:54 +0200 Subject: [PATCH 3/7] Update services/actions/run.go Co-authored-by: delvh Signed-off-by: ChristopherHX --- services/actions/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/actions/run.go b/services/actions/run.go index 39e63ad456578..af7ef571c7d0b 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -17,7 +17,7 @@ import ( "gopkg.in/yaml.v3" ) -// PrepareRun prepares a run before inserting it into the database +// PrepareRun prepares a run and inserts it into the database // It parses the workflow content, evaluates concurrency if needed, and inserts the run and its jobs into the database. // The title will be cut off at 255 characters if it's longer than 255 characters. func PrepareRun(ctx context.Context, content []byte, run *actions_model.ActionRun, inputsWithDefaults map[string]any) error { From 0cc6d31fdb02052d8b98d82c2ec692e8b4265296 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 11 Oct 2025 21:10:49 +0200 Subject: [PATCH 4/7] rename func --- services/actions/notifier_helper.go | 2 +- services/actions/run.go | 4 ++-- services/actions/schedule_tasks.go | 2 +- services/actions/workflow.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 3f921b62c5856..ca7c304b2593b 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -344,7 +344,7 @@ func handleWorkflows( run.NeedApproval = need - if err := PrepareRun(ctx, dwf.Content, run, nil); err != nil { + if err := PrepareRunAndInsert(ctx, dwf.Content, run, nil); err != nil { log.Error("PrepareRun: %v", err) continue } diff --git a/services/actions/run.go b/services/actions/run.go index af7ef571c7d0b..a82b74ac15eb5 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -17,10 +17,10 @@ import ( "gopkg.in/yaml.v3" ) -// PrepareRun prepares a run and inserts it into the database +// PrepareRunAndInsert prepares a run and inserts it into the database // It parses the workflow content, evaluates concurrency if needed, and inserts the run and its jobs into the database. // The title will be cut off at 255 characters if it's longer than 255 characters. -func PrepareRun(ctx context.Context, content []byte, run *actions_model.ActionRun, inputsWithDefaults map[string]any) error { +func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model.ActionRun, inputsWithDefaults map[string]any) error { if err := run.LoadAttributes(ctx); err != nil { return fmt.Errorf("LoadAttributes: %w", err) } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 17e52a5865485..037bf5cddd187 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -119,7 +119,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) // FIXME cron.Content might be outdated if the workflow file has been changed. // Load the latest sha from default branch // Insert the action run and its associated jobs into the database - if err := PrepareRun(ctx, cron.Content, run, nil); err != nil { + if err := PrepareRunAndInsert(ctx, cron.Content, run, nil); err != nil { return err } diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 1547342ff1fed..25801d6fa1da1 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -166,7 +166,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re run.EventPayload = string(eventPayload) // Insert the action run and its associated jobs into the database - if err := PrepareRun(ctx, content, run, inputsWithDefaults); err != nil { + if err := PrepareRunAndInsert(ctx, content, run, inputsWithDefaults); err != nil { return fmt.Errorf("PrepareRun: %w", err) } return nil From a5aea1b1e2d6ddd9248b701770093992c3e1cff4 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 11 Oct 2025 21:12:47 +0200 Subject: [PATCH 5/7] do not query vars twice --- services/actions/run.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/services/actions/run.go b/services/actions/run.go index a82b74ac15eb5..22d7410debc5b 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -53,7 +53,7 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model run.Title = jobs[0].RunName } - if err := InsertRun(ctx, run, jobs); err != nil { + if err := InsertRun(ctx, run, jobs, vars); err != nil { return fmt.Errorf("InsertRun: %w", err) } @@ -81,7 +81,7 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model // InsertRun inserts a run // The title will be cut off at 255 characters if it's longer than 255 characters. -func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow) error { +func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow, vars map[string]string) error { return db.WithTx(ctx, func(ctx context.Context) error { index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) if err != nil { @@ -108,12 +108,6 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar return err } - // query vars for evaluating job concurrency groups - vars, err := actions_model.GetVariablesOfRun(ctx, run) - if err != nil { - return fmt.Errorf("get run %d variables: %w", run.ID, err) - } - runJobs := make([]*actions_model.ActionRunJob, 0, len(jobs)) var hasWaitingJobs bool for _, v := range jobs { From f8ad8c0a58d1a6a17dd446a9300df0a12bd82820 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Oct 2025 11:50:51 +0800 Subject: [PATCH 6/7] Update services/actions/notifier_helper.go Signed-off-by: wxiaoguang --- services/actions/notifier_helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index ca7c304b2593b..d17955b029081 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -345,7 +345,7 @@ func handleWorkflows( run.NeedApproval = need if err := PrepareRunAndInsert(ctx, dwf.Content, run, nil); err != nil { - log.Error("PrepareRun: %v", err) + log.Error("PrepareRunAndInsert: %v", err) continue } } From 1a52277c5c182f7cf2e9d3f99d3730c9044ba8de Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Oct 2025 12:03:04 +0800 Subject: [PATCH 7/7] fix comment and fixme --- services/actions/run.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/services/actions/run.go b/services/actions/run.go index 22d7410debc5b..e8719533d09f5 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -9,7 +9,6 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" @@ -53,29 +52,27 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model run.Title = jobs[0].RunName } - if err := InsertRun(ctx, run, jobs, vars); err != nil { + if err = InsertRun(ctx, run, jobs, vars); err != nil { return fmt.Errorf("InsertRun: %w", err) } - // FIXME PERF do we need this db round trip? + // Load the newly inserted jobs with all fields from database (the job models in InsertRun are partial, so load again) allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) if err != nil { - log.Error("FindRunJobs: %v", err) + return fmt.Errorf("FindRunJob: %w", err) } // FIXME PERF skip this for schedule, dispatch etc. - CreateCommitStatus(ctx, allJobs...) - - err = run.LoadAttributes(ctx) - if err != nil { - log.Error("LoadAttributes: %v", err) + const shouldCreateCommitStatus = true + if shouldCreateCommitStatus { + CreateCommitStatus(ctx, allJobs...) } + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) for _, job := range allJobs { notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) } - // Return nil if no errors occurred return nil }