mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-03-26 18:13:04 -04:00
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Previously, Forgejo's behaviour for an Actions reusable workflow was to send the entire job to one specific Forgejo Runner based upon its required `runs-on` label, and that single Runner would then read the workflow file and perform all the jobs inside simultaneously, merging their log output into one output (#9768). This PR begins an implementation of expanding reusable workflows into their internal jobs. In this PR, the most basic support is implemented for expanding reusable workflows: - If a `runs-on` field is provided on the workflow, then the legacy behaviour of sending the reusable workflow to a runner is maintained. - If the `runs-on` field is omitted, then the job may be expanded, if: - If the `uses:` is a local path within the repo -- expanded - If the `uses:` is a path to another repo that is on the same Forgejo server -- expanded - If the `uses:` is a fully-qualified URL -- not expanded Because this is an "opt-in" implementation by omitting `runs-on`, and all existing capability is retained, I've **omitted some features** from this PR to make the scope small and manageable for review and testing. These features will be implemented after the initial support is landed: - Workflow input variables - Workflow secrets - Workflow output variables - "Incomplete" workflows which require multiple passes to evaluate -- any job within a reusable workflow where the `with`, `runs-on`, or `strategy.matrix` fields contain an output from another job with `${{ needs... }}` Although this implementation has restrictions with missing features, it is intended to fix #9768. Replaces PR #10448. ## 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)). - end-to-end testing: https://code.forgejo.org/forgejo/end-to-end/pulls/1316 ### Documentation - [x] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - https://codeberg.org/forgejo/docs/pulls/1648 - [ ] 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. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10525 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
267 lines
7.5 KiB
Go
267 lines
7.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/v12/act/jobparser"
|
|
act_model "code.forgejo.org/forgejo/runner/v12/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 {
|
|
WorkflowDirectory string
|
|
WorkflowID string
|
|
Ref string
|
|
Commit *git.Commit
|
|
GitEntry *git.TreeEntry
|
|
}
|
|
|
|
type InputValueGetter func(key string) string
|
|
|
|
var ErrSkipDispatchInput = errors.New("skip dispatching of input")
|
|
|
|
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}
|
|
}
|
|
return "", ErrSkipDispatchInput
|
|
}
|
|
} 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) WorkflowPath() string {
|
|
return entry.WorkflowDirectory + "/" + entry.WorkflowID
|
|
}
|
|
|
|
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 := entry.WorkflowPath()
|
|
|
|
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 == ErrSkipDispatchInput {
|
|
continue
|
|
} else 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("too 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,
|
|
WorkflowDirectory: entry.WorkflowDirectory,
|
|
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),
|
|
// We don't have any job outputs yet, but `WithJobOutputs(...)` triggers JobParser to supporting its
|
|
// `IncompleteMatrix` tagging for any jobs that require the inputs of other jobs.
|
|
jobparser.WithJobOutputs(map[string]map[string]string{}),
|
|
jobparser.SupportIncompleteRunsOn(),
|
|
jobparser.ExpandLocalReusableWorkflows(expandLocalReusableWorkflows(entry.Commit)),
|
|
jobparser.ExpandInstanceReusableWorkflows(expandInstanceReusableWorkflows(ctx)),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
|
|
return run, jobNames, err
|
|
}
|
|
|
|
return run, jobNames, consistencyCheckRun(ctx, run)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
workflowDirectory, 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{
|
|
WorkflowDirectory: workflowDirectory,
|
|
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
|
|
}
|