mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-07 07:40:29 +00:00
Contains a number of small bug fixes where jobs behaved differently from GitHub or failed silently.
1. Hooks fail on a refused `::set-env::` or `::add-path::` and no longer inherit the last step's identity
1. Docker build, exec and wait errors are returned instead of swallowed, token-only registry credentials work, logs of `--tty` services decode
1. Env files split lines like GitHub on each OS, so a carriage return is kept on Linux and CRLF ends a line on Windows, empty heredoc names or delimiters are rejected
1. Post steps run after their pre step even when main was skipped, composites see undeclared inputs, `runs.args` wins without `with.args` being parsed
1. Docker actions skip evaluating defaults of inputs given in `with` and read `entryPoint` and `args` in any case, a lowercase `dockerfile` is found, undeclared inputs get GitHub's warning
1. A user-set `CI` is kept, `GITHUB_REPOSITORY_ID` and the other missing `GITHUB_*` variables are exported
1. `timeout-minutes` can read the step's own `env`, a step-level `ACTIONS_ALLOW_UNSECURE_COMMANDS: false` beats a job-level `true` while the runner's own opt-in still wins, `pwsh` quotes the script path
1. A step `env:` given as one expression such as `${{ fromJSON(matrix.env) }}` is applied, `hashFiles` fails with GitHub's messages on script errors and after 120 seconds, a job image without node still gets an empty hash
1. A step whose command files cannot be reset fails instead of importing stale ones, a timeout between steps fails the job instead of reporting success
1. One named volume can be mounted at several paths, host-mode PTY steps keep their last output when they fail, host scratch directories are removed when job setup fails
1. Cancelling a step right after it starts kills its whole process tree
1. Cache prefix restores pick the newest entry created within the same second, also on S3, cache commits with a wrong size are rejected
1. Artifact chunks are written at their offset, download URLs are escaped, listing a run without artifacts no longer crashes
1. Poll backoff reaches `fetch_interval_max`, proxy passwords are masked in more forms, secrets are also masked in step names and in their quoted form in the runner log, a failed final report is logged
1. On Windows hosts a differently cased unsecure-commands opt-in or proxy variable counts, and an early cancel no longer leaks a job object handle
1. Interactive `register` prompts for a missing token and stops on SIGTERM, `exec --list --detect-event` no longer panics without events, `config set` works on `cache.s3` keys
Reviewed-on: https://gitea.com/gitea/runner/pulls/1338
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
346 lines
11 KiB
Go
346 lines
11 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"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
"gitea.com/gitea/runner/act/common/git"
|
|
|
|
"gitea.dev/actionslib/pkg/model"
|
|
)
|
|
|
|
type stepActionRemote struct {
|
|
Step *model.Step
|
|
RunContext *RunContext
|
|
compositeRunContext *RunContext
|
|
compositeSteps *compositeSteps
|
|
readAction readAction
|
|
runAction runAction
|
|
action *model.Action
|
|
env map[string]string
|
|
remoteAction *remoteAction
|
|
actionDir string // the repository at the resolved commit, shared by the job's actions in it
|
|
resolvedSha string // set on the first step preparing the repository
|
|
preStepStarted bool
|
|
}
|
|
|
|
type actionDownload struct {
|
|
dir, sha string
|
|
err error
|
|
fetched chan struct{}
|
|
}
|
|
|
|
var stepActionRemoteFetch = git.FetchAction
|
|
|
|
func (sar *stepActionRemote) fetchAction(ctx context.Context, remote *remoteAction, token string) (string, string, error) {
|
|
cloneURL := remote.CloneURL(sar.RunContext.Config.DefaultActionURL())
|
|
return stepActionRemoteFetch(ctx, sar.RunContext.Config.withInstanceAuth(git.NewGitCloneExecutorInput{
|
|
URL: cloneURL,
|
|
Ref: remote.Ref,
|
|
Dir: filepath.Join(sar.RunContext.ActionCacheDir(), model.UsesHash(remote.downloadKey(sar.RunContext.Config.DefaultActionURL()))),
|
|
OfflineMode: sar.RunContext.Config.ActionOfflineMode,
|
|
Depth: sar.RunContext.Config.ActionCloneDepth,
|
|
}, token))
|
|
}
|
|
|
|
func (sar *stepActionRemote) skipsLocalCheckout(remote *remoteAction, github *model.GithubContext) bool {
|
|
return remote.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout
|
|
}
|
|
|
|
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|
return func(ctx context.Context) error {
|
|
if sar.remoteAction != nil && sar.action != nil {
|
|
// we are already good to run
|
|
return nil
|
|
}
|
|
|
|
// For gitea:
|
|
// Since actions can specify the download source via a url prefix.
|
|
// The prefix may contain some sensitive information that needs to be stored in secrets,
|
|
// so we need to interpolate the expression value for uses first.
|
|
uses, err := sar.RunContext.NewExpressionEvaluator(ctx).Interpolate(ctx, sar.Step.Uses)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to interpolate uses: %w", err)
|
|
}
|
|
sar.Step.Uses = uses
|
|
|
|
github := sar.getGithubContext(ctx) // read before remoteAction is set, so `$/` resolves against the enclosing action
|
|
if sar.remoteAction, err = newRemoteAction(sar.Step.Uses, github, sar.RunContext.Config.GitHubInstance); err != nil {
|
|
return err
|
|
}
|
|
|
|
if sar.skipsLocalCheckout(sar.remoteAction, github) {
|
|
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
|
|
return nil
|
|
}
|
|
job := sar.RunContext.topLevelRunContext()
|
|
key := sar.remoteAction.downloadKey(sar.RunContext.Config.DefaultActionURL())
|
|
download := job.actionDownloads[key]
|
|
if download == nil {
|
|
download = &actionDownload{}
|
|
// For Gitea
|
|
// github.Token, unlike Config.GetToken, survives the composite config copy, which nils Config.Secrets.
|
|
download.dir, download.sha, download.err = sar.fetchAction(ctx, sar.remoteAction, github.Token)
|
|
if job.actionDownloads == nil {
|
|
job.actionDownloads = map[string]*actionDownload{}
|
|
}
|
|
job.actionDownloads[key] = download
|
|
} else if download.fetched != nil {
|
|
select {
|
|
case <-download.fetched:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
sar.resolvedSha, download.sha = download.sha, ""
|
|
var refErr *git.Error
|
|
switch {
|
|
case errors.As(download.err, &refErr) && errors.Is(download.err, git.ErrShortRef):
|
|
return fmt.Errorf("unable to resolve action `%s`, the provided ref `%s` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `%s` instead",
|
|
sar.Step.Uses, sar.remoteAction.Ref, refErr.Commit())
|
|
case download.err != nil:
|
|
return download.err
|
|
}
|
|
sar.actionDir = download.dir
|
|
|
|
unlock, err := git.AcquireCloneLock(ctx, sar.actionDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unlock()
|
|
|
|
remoteReader := func(filename string) (io.Reader, io.Closer, error) {
|
|
f, err := os.Open(filepath.Join(sar.actionDir, sar.remoteAction.Path, filename))
|
|
return f, f, err
|
|
}
|
|
|
|
actionModel, err := sar.readAction(ctx, sar.Step, sar.actionDir, sar.remoteAction.Path, remoteReader, os.WriteFile)
|
|
sar.action = actionModel
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (sar *stepActionRemote) actionDownloadInfo() (reference, sha string) { // once per job, as actions/runner reports
|
|
if sar.resolvedSha == "" {
|
|
return "", ""
|
|
}
|
|
return fmt.Sprintf("%s/%s@%s", sar.remoteAction.Org, sar.remoteAction.Repo, sar.remoteAction.Ref), sar.resolvedSha
|
|
}
|
|
|
|
func (sar *stepActionRemote) pre() common.Executor {
|
|
sar.env = map[string]string{}
|
|
|
|
return common.NewPipelineExecutor(
|
|
sar.prepareActionExecutor(),
|
|
runStepExecutor(sar, stepStagePre, func(ctx context.Context) error {
|
|
sar.preStepStarted = true
|
|
return runPreStep(sar)(ctx)
|
|
}).If(hasPreStep(sar)).If(shouldRunPreStep(sar)))
|
|
}
|
|
|
|
func (sar *stepActionRemote) main() common.Executor {
|
|
return common.NewPipelineExecutor(
|
|
sar.prepareActionExecutor(),
|
|
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
|
printRunActionHeader(ctx, sar.Step, sar.env, sar.RunContext)
|
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
|
defer rawLogger.Infof("::endgroup::")
|
|
|
|
github := sar.getGithubContext(ctx)
|
|
if sar.skipsLocalCheckout(sar.remoteAction, github) {
|
|
if sar.RunContext.Config.BindWorkdir {
|
|
common.Logger(ctx).Debugf("Skipping local actions/checkout because you bound your workspace")
|
|
return nil
|
|
}
|
|
copyToPath := path.Join(sar.RunContext.JobContainer.ToContainerPath(sar.RunContext.Config.Workdir), sar.Step.With["path"])
|
|
return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore, false)(ctx)
|
|
}
|
|
|
|
return sar.runAction(sar, sar.actionDir, sar.remoteAction)(ctx)
|
|
}),
|
|
)
|
|
}
|
|
|
|
func (sar *stepActionRemote) post() common.Executor {
|
|
return runStepExecutor(sar, stepStagePost, runPostStep(sar)).If(hasPostStep(sar)).If(shouldRunPostStep(sar))
|
|
}
|
|
|
|
func (sar *stepActionRemote) containerKey() string {
|
|
return model.UsesHash(sar.remoteAction.URL + "/" + sar.remoteAction.Reference())
|
|
}
|
|
|
|
func (sar *stepActionRemote) getRunContext() *RunContext {
|
|
return sar.RunContext
|
|
}
|
|
|
|
func (sar *stepActionRemote) getGithubContext(ctx context.Context) *model.GithubContext {
|
|
ghc := sar.getRunContext().getGithubContext(ctx)
|
|
|
|
// extend github context if we already have an initialized remoteAction
|
|
remoteAction := sar.remoteAction
|
|
if remoteAction != nil {
|
|
ghc.ActionRepository = fmt.Sprintf("%s/%s", remoteAction.Org, remoteAction.Repo)
|
|
ghc.ActionRef = remoteAction.Ref
|
|
}
|
|
|
|
return ghc
|
|
}
|
|
|
|
func (sar *stepActionRemote) getStepModel() *model.Step {
|
|
return sar.Step
|
|
}
|
|
|
|
func (sar *stepActionRemote) getEnv() *map[string]string {
|
|
return &sar.env
|
|
}
|
|
|
|
func (sar *stepActionRemote) getIfExpression(ctx context.Context, stage stepStage) string {
|
|
switch stage {
|
|
case stepStagePre:
|
|
github := sar.getGithubContext(ctx)
|
|
if sar.skipsLocalCheckout(sar.remoteAction, github) {
|
|
// skip local checkout pre step
|
|
return "false"
|
|
}
|
|
return sar.action.Runs.PreIf
|
|
case stepStageMain:
|
|
return sar.Step.If.Value
|
|
case stepStagePost:
|
|
return sar.action.Runs.PostIf
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (sar *stepActionRemote) getActionModel() *model.Action {
|
|
return sar.action
|
|
}
|
|
|
|
func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) (*RunContext, error) {
|
|
if sar.compositeRunContext == nil {
|
|
_, containerActionDir := getContainerActionPaths(sar)
|
|
|
|
compositeRunContext, err := newCompositeRunContext(ctx, sar.RunContext, sar, containerActionDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sar.compositeRunContext = compositeRunContext
|
|
sar.compositeSteps = compositeRunContext.compositeExecutor(sar.action)
|
|
} else {
|
|
// Re-evaluate environment here. For remote actions the environment
|
|
// need to be re-created for every stage (pre, main, post) as there
|
|
// might be required context changes (inputs/outputs) while the action
|
|
// stages are executed. (e.g. the output of another action is the
|
|
// input for this action during the main stage, but the env
|
|
// was already created during the pre stage)
|
|
env, err := evaluateCompositeInputAndEnv(ctx, sar.RunContext, sar)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sar.compositeRunContext.setCompositeActionEnv(env)
|
|
sar.compositeRunContext.ExtraPath = sar.RunContext.ExtraPath
|
|
}
|
|
return sar.compositeRunContext, nil
|
|
}
|
|
|
|
func (sar *stepActionRemote) getCompositeSteps() *compositeSteps {
|
|
return sar.compositeSteps
|
|
}
|
|
|
|
type remoteAction struct {
|
|
URL string
|
|
Org string
|
|
Repo string
|
|
Path string
|
|
Ref string
|
|
}
|
|
|
|
func (ra *remoteAction) downloadKey(defaultURL string) string {
|
|
return ra.CloneURL(defaultURL) + "@" + ra.Ref
|
|
}
|
|
|
|
func (ra *remoteAction) CloneURL(u string) string {
|
|
if ra.URL == "" {
|
|
// keep an absolute local path as-is (used by tests to resolve actions from a local
|
|
// repo); only bare host names get the https:// scheme prepended
|
|
if !filepath.IsAbs(u) {
|
|
u = withHTTPS(u)
|
|
}
|
|
} else {
|
|
u = ra.URL
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s/%s", u, ra.Org, ra.Repo)
|
|
}
|
|
|
|
// Reference renders the action as {org}/{repo}[/path]@{ref}, omitting the download source, which
|
|
// can be interpolated from a secret.
|
|
func (ra *remoteAction) Reference() string {
|
|
repo := fmt.Sprintf("%s/%s", ra.Org, ra.Repo)
|
|
if ra.Path != "" {
|
|
repo = fmt.Sprintf("%s/%s", repo, ra.Path)
|
|
}
|
|
return fmt.Sprintf("%s@%s", repo, ra.Ref)
|
|
}
|
|
|
|
func (ra *remoteAction) IsCheckout() bool {
|
|
return ra.Org == "actions" && ra.Repo == "checkout"
|
|
}
|
|
|
|
// newRemoteAction resolves `self:` on instanceURL and `$/` in the enclosing composite action, else the workflow's repo and commit.
|
|
func newRemoteAction(action string, github *model.GithubContext, instanceURL string) (*remoteAction, error) {
|
|
uses, err := model.ParseActionUses(action)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ra := &remoteAction{URL: uses.URL, Org: uses.Owner, Repo: uses.Repo, Path: uses.Path, Ref: uses.Ref}
|
|
switch uses.Kind {
|
|
case model.ActionUsesInstance:
|
|
if instanceURL == "" {
|
|
return nil, fmt.Errorf("unable to resolve %q without a Gitea instance", action)
|
|
}
|
|
ra.URL = instanceURL
|
|
if !strings.Contains(instanceURL, "://") {
|
|
ra.URL = "https://" + instanceURL
|
|
}
|
|
case model.ActionUsesSelfRepo:
|
|
repo, ref := github.ActionRepository, github.ActionRef
|
|
if repo == "" || ref == "" {
|
|
repo, ref = github.Repository, github.Sha
|
|
}
|
|
ra.Org, ra.Repo, _ = strings.Cut(repo, "/")
|
|
ra.URL, ra.Ref = github.ServerURL, ref
|
|
if ra.URL == "" {
|
|
return nil, fmt.Errorf("unable to resolve %q without a Gitea instance", action)
|
|
}
|
|
if ra.Org == "" || ra.Repo == "" || ra.Ref == "" {
|
|
return nil, fmt.Errorf("unable to resolve %q without a repository and commit", action)
|
|
}
|
|
}
|
|
return ra, nil
|
|
}
|
|
|
|
func safeFilename(s string) string {
|
|
return strings.NewReplacer(
|
|
`<`, "-",
|
|
`>`, "-",
|
|
`:`, "-",
|
|
`"`, "-",
|
|
`/`, "-",
|
|
`\`, "-",
|
|
`|`, "-",
|
|
`?`, "-",
|
|
`*`, "-",
|
|
).Replace(s)
|
|
}
|