mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-16 15:56:34 +00:00
Partial backport of #9434. Fixes a bug: it was intended that pushes to a branch and pushes to a pull request would cancel any previous running job from the same workflow, however this functionality only worked on a `on: push` workflow. This PR fixes it for an `on: pull_request` workflow. Adds a feature: `concurrency.cancel-in-progress` can be used to override the automatic cancellation behaviour and disable it. It can be disabled unconditionally in a workflow: ```yaml concurrency: cancel-in-progress: false ``` Or it can be disabled with some logic; for example, keeping the cancel behaviour on PRs but disabling it otherwise: ```yaml concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} ``` Only a small subset of automated tests were applicable for backport, so I supplemented with manual testing in these cases: | | Expected | Actual | | ------------------------------------------------------------------ | ------------- | ------------- | | Default behaviour: | | | | on: pull_request -- push to a PR when it's already running | Cancelled | Cancelled | | on: push -- push to a branch (eg. main) when it's already running | Cancelled | Cancelled | | on: workflow_dispatch -- run again when it is already running | Multiple Runs | Multiple Runs | | | | | | Set cancel-in-progress: true | | | | on: pull_request -- push to a PR when it's already running | Cancelled | Cancelled | | on: push -- push to a branch (eg. main) when it's already running | Cancelled | Cancelled | | on: workflow_dispatch -- run again when it is already running | Multiple Runs | Multiple Runs | | | | | | Set cancel-in-progress: false | | | | on: pull_request -- push to a PR when it's already running | Multiple Runs | Multiple Runs | | on: push -- push to a branch (eg. main) when it's already running | Multiple Runs | Multiple Runs | | on: workflow_dispatch -- run again when it is already running | Multiple Runs | Multiple Runs | | | | | | Set cancel-in-progress: ${{ github.event_name == 'pull_request' }} | | | | on: pull_request -- push to a PR when it's already running | Cancelled | Cancelled | | on: push -- push to a branch (eg. main) when it's already running | Multiple Runs | Multiple Runs | | on: workflow_dispatch -- run again when it is already running | Multiple Runs | Multiple Runs | ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - **TODO:** Will backport the relevant documentation. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/9797): <!--number 9797 --><!--line 0 --><!--description ZmVhdDogYWxsb3cgd29ya2Zsb3dzIHRvIGNvbnRyb2wgY2FuY2VsbGF0aW9uIG9mIGV4aXN0aW5nIGpvYnM=-->feat: allow workflows to control cancellation of existing jobs<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9797 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Reviewed-by: Michael Kriese <michael.kriese@gmx.de> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
208 lines
5.5 KiB
Go
208 lines
5.5 KiB
Go
// 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)
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 {
|
|
_, 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 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 {
|
|
// A workflow has explicitly listed `cancel-in-progress: false`, and we don't support concurrency groups
|
|
// (queue-behind style behaviour, to be added in Forgejo v14).
|
|
run.ConcurrencyType = actions_model.UnlimitedConcurrency
|
|
}
|
|
return nil
|
|
}
|