mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
Previously, when triggering workflows with `workflow_dispatch`, Forgejo only interpreted `on` as boolean `true`. Everything else, including `true`, was treated as `false`. This behaviour does not match the [Forgejo documentation](https://forgejo.org/docs/v13.0/user/actions/reference/#onworkflow_dispatch) that states that `true` and `false` are permitted values. It is also outside the [YAML 1.2 specification of booleans](https://yaml.org/spec/1.2.2/#10212-boolean) that only permits `true` and `false`. After this change, only `true` and `false` have the desired effect. `on` (converted to `true`) is kept for compatibility reasons to give people time to upgrade. This problem only affected users of the Forgejo API, because the UI sent the expected values. Resolves forgejo/forgejo#10070 by fixing the documentation mismatch. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10089 Reviewed-by: klausfyhn <klausfyhn@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch> Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
242 lines
6.5 KiB
Go
242 lines
6.5 KiB
Go
// Copyright The Forgejo Authors.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
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 resolveDispatchInput(key, value string, input act_model.WorkflowDispatchInput) (string, error) {
|
|
if len(value) == 0 {
|
|
value = input.Default
|
|
if len(value) == 0 {
|
|
if input.Required {
|
|
name := input.Description
|
|
if len(name) == 0 {
|
|
name = key
|
|
}
|
|
return "", InputRequiredErr{Name: name}
|
|
}
|
|
}
|
|
} else if input.Type == "boolean" {
|
|
// Temporary compatibility shim for people that upgrade to Forgejo 14. Can be removed with Forgejo 15.
|
|
if value == "on" {
|
|
value = "true"
|
|
}
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
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 {
|
|
value, err := resolveDispatchInput(key, inputGetter(key), input)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
inputs[key] = value
|
|
inputsAny[key] = value
|
|
}
|
|
}
|
|
|
|
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 := actions.JobParser(content, jobparser.WithVars(vars), jobparser.WithInputs(inputsAny))
|
|
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
|
|
}
|