Files
act_runner/act/runner/reusable_workflow.go
silverwind d882311ec6 refactor: remove dead code and consolidate tests (#1335)
Removes dead code and redundant logic and consolidates tests, so there is less to read and maintain. No behavior change.

1. **act/runner**: inline single-use wrappers, compile regexes once instead of per call, embed the trampoline as a string, drop the unused legacy result of `parseActionCommand`
2. **act/container**: `sanitizeConfig` and `mergeContainerConfigs` stop returning the configs they already mutate in place, drop never-set option fields and the unused `user` parameter of `ExecWithCmdLine`
3. **act/common and cache server**: drop the unused `Trace`/`Tracef` loggers and the short SHA of `FindGitRevision`, `RegisterJob` releases through `RevokeJob`
4. **daemon, poller and reporter**: one shutdown wait for both poll modes, drop an unreachable fetch timeout branch, guard the reporter's last report time with its existing lock instead of an atomic
5. **build**: drop unused Makefile and release workflow variables, collapse the goreleaser ignore list to the same build targets
6. **tests**: fold near-duplicate tests into tables with shared setup

Throughout, hand-rolled logic gives way to `cmp.Or`, `strings.Cut`, `slices` and `min`/`max`.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1335
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-22 19:41:10 +00:00

230 lines
7.7 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"context"
"fmt"
"net/url"
"path"
"regexp"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.dev/actionslib/pkg/model"
)
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
localWorkflow, err := model.ParseReusableWorkflowUses(rc.Run.Job().Uses)
if err != nil {
return common.NewErrorExecutor(err)
}
if !rc.Config.NoSkipCheckout {
// resolve the local workflow against the workspace root, not the process
// working directory, so it is found regardless of where the runner is invoked
return newReusableWorkflowExecutor(rc, rc.Config.Workdir, localWorkflow.Path, false)
}
uses := fmt.Sprintf("%s/%s@%s", rc.Config.PresetGitHubContext.Repository, localWorkflow.Path, rc.Config.PresetGitHubContext.Sha)
remoteReusableWorkflow, err := newRemoteReusableWorkflow(rc.Config.GitHubInstance, uses)
if err != nil {
return common.NewErrorExecutor(err)
}
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(uses))
return common.NewPipelineExecutor(
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir),
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path, true),
)
}
func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
remoteReusableWorkflow, err := newRemoteReusableWorkflow(rc.Config.GitHubInstance, rc.Run.Job().Uses)
if err != nil {
return common.NewErrorExecutor(err)
}
// uses with safe filename makes the target directory look something like this {owner}-{repo}-.github-workflows-{filename}@{ref}
// instead we will just use {owner}-{repo}@{ref} as our target directory. This should also improve performance when we are using
// multiple reusable workflows from the same repository and ref since for each workflow we won't have to clone it again
filename := fmt.Sprintf("%s/%s@%s", remoteReusableWorkflow.Owner, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref)
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename))
return common.NewPipelineExecutor(
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir),
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path, true),
)
}
// cloneRemoteReusableWorkflow always invokes the clone executor — moving refs
// (branches, tags) must be re-resolved each run, matching GitHub Actions.
//
// Callers must not change remoteReusableWorkflow.URL, because:
// 1. Gitea doesn't support specifying GithubContext.ServerURL by the GITHUB_SERVER_URL env
// 2. Gitea has already full URL with rc.Config.GitHubInstance when calling newRemoteReusableWorkflow
//
// remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory string) common.Executor {
return func(ctx context.Context) error {
interpolatedURL, err := rc.NewExpressionEvaluator(ctx).Interpolate(ctx, cloneURL)
if err != nil {
return fmt.Errorf("unable to interpolate the workflow clone URL: %w", err)
}
return git.NewGitCloneExecutor(rc.Config.withInstanceAuth(git.NewGitCloneExecutorInput{
URL: interpolatedURL,
Ref: ref,
Dir: targetDirectory,
OfflineMode: rc.Config.ActionOfflineMode,
Depth: rc.Config.ActionCloneDepth,
}, rc.Config.GetToken()))(ctx)
}
}
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string, cached bool) common.Executor {
return func(ctx context.Context) error {
planner, err := func() (model.WorkflowPlanner, error) {
if cached {
unlock, err := git.AcquireCloneLock(ctx, directory)
if err != nil {
return nil, err
}
defer unlock()
}
return model.NewWorkflowPlanner(path.Join(directory, workflow), true)
}()
if err != nil {
return err
}
plan, err := planner.PlanEvent("workflow_call")
if err != nil {
return err
}
runner, err := (&runnerImpl{
config: rc.Config,
caller: &caller{
runContext: rc,
reusedWorkflowJobResults: map[string]string{},
},
}).configure()
if err != nil {
return err
}
return common.NewPipelineExecutor( // For Gitea
runner.NewPlanExecutor(plan),
setReusedWorkflowCallerResult(rc, runner),
)(ctx)
}
}
type remoteReusableWorkflow struct {
*model.ReusableWorkflowUses
URL string
}
func (r *remoteReusableWorkflow) CloneURL() string {
return fmt.Sprintf("%s/%s/%s", withHTTPS(r.URL), r.Owner, r.Repo)
}
var absoluteReusableWorkflowURLRegex = regexp.MustCompile(`^(https?://.*)/([^/]+/[^/]+/\.[^/]+/workflows/[^@]+@.*)$`)
// For Gitea
// newRemoteReusableWorkflow parses a remote `uses`, an absolute URL up to `{owner}/{repo}/.{git_platform}/workflows/` replaces baseURL
func newRemoteReusableWorkflow(baseURL, uses string) (*remoteReusableWorkflow, error) {
if matches := absoluteReusableWorkflowURLRegex.FindStringSubmatch(uses); matches != nil {
baseURL, uses = matches[1], matches[2]
}
parsed, err := model.ParseReusableWorkflowUses(uses)
if err != nil {
return nil, err
}
if parsed.IsLocal() {
return nil, fmt.Errorf("expected a remote reusable workflow, got %q", uses)
}
return &remoteReusableWorkflow{ReusableWorkflowUses: parsed, URL: baseURL}, nil
}
// For Gitea
func setReusedWorkflowCallerResult(rc *RunContext, runner *runnerImpl) common.Executor {
return func(ctx context.Context) error {
hasFailure := false
for _, result := range runner.caller.reusedWorkflowJobResults {
if result == "pending" {
return nil
}
if result == "failure" {
hasFailure = true
}
}
reusedWorkflowJobResult := "success"
reusedWorkflowJobResultMessage := "succeeded"
if hasFailure {
reusedWorkflowJobResult = "failure"
reusedWorkflowJobResultMessage = "failed"
}
if rc.caller != nil {
rc.caller.setReusedWorkflowJobResult(rc.Run.JobID, reusedWorkflowJobResult)
} else {
// Serialize this shared Job.Result write against the other matrix combos
// and setJobResult (same lockJob key).
unlock := lockJob(rc.Run.Job())
rc.result(reusedWorkflowJobResult)
unlock()
common.Logger(ctx).With("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
}
return nil
}
}
// For Gitea
// withInstanceAuth passes instance credentials only to clones from this Gitea instance, and runner.insecure only to its registered address.
func (c Config) withInstanceAuth(input git.NewGitCloneExecutorInput, token string) git.NewGitCloneExecutorInput {
u, err := url.Parse(input.URL)
if err != nil || !onInstanceHost(u.Host, c.GitHubInstance, c.trustedActionInstance()) {
return input
}
input.ClientCertFile, input.ClientKeyFile = c.ClientCertFile, c.ClientKeyFile
if onInstanceHost(u.Host, c.GitHubInstance) {
input.InsecureSkipTLS, input.Transport = c.InsecureSkipTLS, c.InstanceTransport
}
if u.User == nil {
input.Token = token
}
return input
}
// For Gitea
// trustedActionInstance returns the self-hosted DEFAULT_ACTIONS_URL host that may carry the
// task token, or "" when actions resolve to github.com / a GithubMirror (never trusted).
func (c Config) trustedActionInstance() string {
if c.DefaultActionInstanceIsSelfHosted {
return c.DefaultActionInstance
}
return ""
}
// For Gitea
// onInstanceHost reports whether host is that of one of the instance URLs, which may lack a scheme.
func onInstanceHost(host string, instanceURLs ...string) bool {
for _, candidate := range instanceURLs {
if candidate == "" {
continue
}
if u, err := url.Parse(withHTTPS(candidate)); err == nil && u.Host == host {
return true
}
}
return false
}