// Copyright The Forgejo Authors. // SPDX-License-Identifier: MIT package actions import ( "bytes" "context" "errors" "fmt" "strconv" actions_model "forgejo.org/models/actions" "forgejo.org/models/perm" "forgejo.org/models/perm/access" repo_model "forgejo.org/models/repo" "forgejo.org/models/user" "forgejo.org/modules/actions" "forgejo.org/modules/git" "forgejo.org/modules/json" "forgejo.org/modules/setting" "forgejo.org/modules/structs" "forgejo.org/modules/util" "forgejo.org/modules/webhook" "forgejo.org/services/convert" "code.forgejo.org/forgejo/runner/v11/act/jobparser" act_model "code.forgejo.org/forgejo/runner/v11/act/model" ) type InputRequiredErr struct { Name string } func (err InputRequiredErr) Error() string { return fmt.Sprintf("input required for '%s'", err.Name) } func IsInputRequiredErr(err error) bool { _, ok := err.(InputRequiredErr) return ok } type Workflow struct { WorkflowID string Ref string Commit *git.Commit GitEntry *git.TreeEntry } type InputValueGetter func(key string) string func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGetter, repo *repo_model.Repository, doer *user.User) (r *actions_model.ActionRun, j []string, err error) { content, err := actions.GetContentFromEntry(entry.GitEntry) if err != nil { return nil, nil, err } wf, err := act_model.ReadWorkflow(bytes.NewReader(content), false) if err != nil { return nil, nil, err } fullWorkflowID := ".forgejo/workflows/" + entry.WorkflowID title := wf.Name if len(title) < 1 { title = fullWorkflowID } inputs := make(map[string]string) inputsAny := make(map[string]any) if workflowDispatch := wf.WorkflowDispatchConfig(); workflowDispatch != nil { for key, input := range workflowDispatch.Inputs { val := inputGetter(key) if len(val) == 0 { val = input.Default if len(val) == 0 { if input.Required { name := input.Description if len(name) == 0 { name = key } return nil, nil, InputRequiredErr{Name: name} } continue } } else if input.Type == "boolean" { // Since "boolean" inputs are rendered as a checkbox in html, the value inside the form is "on" val = strconv.FormatBool(val == "on") } inputs[key] = val inputsAny[key] = val } } if int64(len(inputs)) > setting.Actions.LimitDispatchInputs { return nil, nil, errors.New("to many inputs") } jobNames := util.KeysOfMap(wf.Jobs) payload := &structs.WorkflowDispatchPayload{ Inputs: inputs, Ref: entry.Ref, Repository: convert.ToRepo(ctx, repo, access.Permission{AccessMode: perm.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), Workflow: fullWorkflowID, } p, err := json.Marshal(payload) if err != nil { return nil, nil, err } notifications, err := wf.Notifications() if err != nil { return nil, nil, err } run := &actions_model.ActionRun{ Title: title, RepoID: repo.ID, Repo: repo, OwnerID: repo.OwnerID, WorkflowID: entry.WorkflowID, TriggerUserID: doer.ID, TriggerUser: doer, Ref: entry.Ref, CommitSHA: entry.Commit.ID.String(), Event: webhook.HookEventWorkflowDispatch, EventPayload: string(p), TriggerEvent: string(webhook.HookEventWorkflowDispatch), Status: actions_model.StatusWaiting, NotifyEmail: notifications, } vars, err := actions_model.GetVariablesOfRun(ctx, run) if err != nil { return nil, nil, err } err = ConfigureActionRunConcurrency(wf, run, vars, inputsAny) if err != nil { return nil, nil, err } if run.ConcurrencyType == actions_model.CancelInProgress { if err := CancelPreviousWithConcurrencyGroup( ctx, run.RepoID, run.ConcurrencyGroup, ); err != nil { return nil, nil, err } } jobs, err := jobParser(content, jobparser.WithVars(vars)) if err != nil { return nil, nil, err } return run, jobNames, actions_model.InsertRun(ctx, run, jobs) } func GetWorkflowFromCommit(gitRepo *git.Repository, ref, workflowID string) (*Workflow, error) { ref, err := gitRepo.ExpandRef(ref) if err != nil { return nil, err } commit, err := gitRepo.GetCommit(ref) if err != nil { return nil, err } entries, err := actions.ListWorkflows(commit) if err != nil { return nil, err } var workflowEntry *git.TreeEntry for _, entry := range entries { if entry.Name() == workflowID { workflowEntry = entry break } } if workflowEntry == nil { return nil, errors.New("workflow not found") } return &Workflow{ WorkflowID: workflowID, Ref: ref, Commit: commit, GitEntry: workflowEntry, }, nil } // Sets the ConcurrencyGroup & ConcurrencyType on the provided ActionRun based upon the Workflow's `concurrency` data, // or appropriate defaults if not present. func ConfigureActionRunConcurrency(workflow *act_model.Workflow, run *actions_model.ActionRun, vars map[string]string, inputs map[string]any) error { concurrencyGroup, cancelInProgress, err := jobparser.EvaluateWorkflowConcurrency( workflow.RawConcurrency, generateGiteaContextForRun(run), vars, inputs) if err != nil { return fmt.Errorf("unable to evaluate workflow `concurrency` block: %w", err) } if concurrencyGroup != "" { run.SetConcurrencyGroup(concurrencyGroup) } else { run.SetDefaultConcurrencyGroup() } if cancelInProgress == nil { // Maintain compatible behavior from before concurrency groups were implemented -- if `cancel-in-progress` // isn't defined in the workflow, cancel on push & PR sync events. if run.Event == webhook.HookEventPush || run.Event == webhook.HookEventPullRequestSync { run.ConcurrencyType = actions_model.CancelInProgress } else { run.ConcurrencyType = actions_model.UnlimitedConcurrency } } else if *cancelInProgress { run.ConcurrencyType = actions_model.CancelInProgress } else if concurrencyGroup == "" { // A workflow has explicitly listed `cancel-in-progress: false`, but has *not* provided a concurrency group. In // this case we want to trigger a different concurrency behavior -- we won't cancel in-progress builds (we were // asked not to), we won't queue behind other builds (we weren't given a concurrency group so it's reasonable to // assume the user doesn't want a concurrency limit). run.ConcurrencyType = actions_model.UnlimitedConcurrency } else { run.ConcurrencyType = actions_model.QueueBehind } return nil }