mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-11-02 05:16:47 +08:00
Closes https://gitea.com/gitea/runner/issues/1317, which makes https://github.com/go-gitea/gitea/issues/39813 redundant as the runner sets these variables. Every built-in `GITHUB_*` environment variable the runner sets is now also available as `GITEA_*` with the same value, so Gitea-native workflows don't have to rely on GitHub naming. The `GITEA_*` names mirror the `GITHUB_*` ones after their last write, so they stay equal and are unset together. Workflow, job and runner `envs` values for these `GITEA_*` names are now overwritten, the same as for `GITHUB_*`. --------- Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1358 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: KenanZhu <imnanoki@qq.com>
377 lines
13 KiB
Go
377 lines
13 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
maps0 "maps"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
"gitea.com/gitea/runner/act/container"
|
|
"gitea.com/gitea/runner/internal/pkg/telemetry"
|
|
|
|
"gitea.dev/actionslib/pkg/exprparser"
|
|
"gitea.dev/actionslib/pkg/model"
|
|
)
|
|
|
|
type step interface {
|
|
pre() common.Executor
|
|
main() common.Executor
|
|
post() common.Executor
|
|
|
|
getRunContext() *RunContext
|
|
getStepModel() *model.Step
|
|
getEnv() *map[string]string
|
|
getIfExpression(context context.Context, stage stepStage) string
|
|
}
|
|
|
|
func stepActionIdentity(step step) (repository, ref string) {
|
|
if action, ok := step.(interface{ actionIdentity() (string, string) }); ok {
|
|
return action.actionIdentity()
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func getStepGithubContext(ctx context.Context, step step) *model.GithubContext {
|
|
github := step.getRunContext().getGithubContext(ctx)
|
|
github.ActionRepository, github.ActionRef = stepActionIdentity(step)
|
|
return github
|
|
}
|
|
|
|
type stepStage int
|
|
|
|
const (
|
|
stepStagePre stepStage = iota
|
|
stepStageMain
|
|
stepStagePost
|
|
)
|
|
|
|
// Controls how many symlinks are resolved for local and remote Actions
|
|
const maxSymlinkDepth = 10
|
|
|
|
func (s stepStage) String() string {
|
|
switch s {
|
|
case stepStagePre:
|
|
return StagePre
|
|
case stepStageMain:
|
|
return StageMain
|
|
case stepStagePost:
|
|
return StagePost
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
func processRunnerEnvFileCommand(ctx context.Context, fileName string, rc *RunContext, setter func(context.Context, map[string]string, string)) error {
|
|
env := map[string]string{}
|
|
err := rc.JobContainer.UpdateFromEnv(path.Join(rc.JobContainer.GetActPath(), fileName), &env)(ctx)
|
|
for k, v := range env {
|
|
setter(ctx, map[string]string{"name": k}, v)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func runStepExecutor(step step, stage stepStage, executor common.Executor) common.Executor {
|
|
return func(ctx context.Context) (err error) {
|
|
logger := common.Logger(ctx)
|
|
rc := step.getRunContext()
|
|
stepModel := step.getStepModel()
|
|
|
|
stepResult := &model.StepResult{
|
|
Outcome: model.StepStatusSuccess,
|
|
Conclusion: model.StepStatusSuccess,
|
|
Outputs: make(map[string]string),
|
|
}
|
|
ctx, endStep := telemetry.StartStep(ctx, stepModel, stage.String())
|
|
defer func() { endStep(err, stepResult) }()
|
|
|
|
ifExpression := step.getIfExpression(ctx, stage)
|
|
rc.CurrentStep = stepModel.ID
|
|
|
|
if stage == stepStageMain {
|
|
rc.StepResults[rc.CurrentStep] = stepResult
|
|
}
|
|
|
|
err = setupEnv(ctx, step)
|
|
var runStep bool
|
|
if err == nil {
|
|
runStep, err = isStepEnabled(ctx, ifExpression, step, stage)
|
|
}
|
|
if err == nil && runStep {
|
|
env := *step.getEnv()
|
|
env["GITHUB_ACTION_REPOSITORY"], env["GITHUB_ACTION_REF"] = stepActionIdentity(step)
|
|
err = setupInputs(ctx, step)
|
|
}
|
|
if err != nil {
|
|
stepResult.Conclusion = model.StepStatusFailure
|
|
stepResult.Outcome = model.StepStatusFailure
|
|
logger.With("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepModel)
|
|
return err
|
|
}
|
|
|
|
if !runStep {
|
|
stepResult.Conclusion = model.StepStatusSkipped
|
|
stepResult.Outcome = model.StepStatusSkipped
|
|
logger.With("stepResult", stepResult.Conclusion).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
|
return nil
|
|
}
|
|
|
|
stepString := rc.ExprEval.InterpolateName(ctx, stepModel.String())
|
|
if strings.Contains(stepString, "::add-mask::") {
|
|
stepString = "add-mask command"
|
|
}
|
|
startLogger := logger.With(StepStartedField, true)
|
|
if stage == stepStageMain {
|
|
// Main steps print their own raw "Run <title>" header, so this line is redundant and
|
|
// only leaks into the "Set up job" section for the first step; keep it as a debug trace.
|
|
startLogger.Debugf("Run %s %s", stage, stepString)
|
|
} else {
|
|
startLogger.Infof("Run %s %s", stage, stepString)
|
|
}
|
|
|
|
// Prepare and clean Runner File Commands
|
|
actPath := rc.JobContainer.GetActPath()
|
|
|
|
outputFileCommand := path.Join("workflow", "outputcmd.txt")
|
|
(*step.getEnv())["GITHUB_OUTPUT"] = path.Join(actPath, outputFileCommand)
|
|
|
|
stateFileCommand := path.Join("workflow", "statecmd.txt")
|
|
(*step.getEnv())["GITHUB_STATE"] = path.Join(actPath, stateFileCommand)
|
|
|
|
pathFileCommand := path.Join("workflow", "pathcmd.txt")
|
|
(*step.getEnv())["GITHUB_PATH"] = path.Join(actPath, pathFileCommand)
|
|
|
|
envFileCommand := path.Join("workflow", "envs.txt")
|
|
(*step.getEnv())["GITHUB_ENV"] = path.Join(actPath, envFileCommand)
|
|
|
|
// Per-step summary file. Composite sub-steps share the outer job step's index
|
|
// via the Parent chain so all writes from within a composite action accumulate
|
|
// in the same file and upload under the outer step_index.
|
|
topRC := rc.topLevelRunContext()
|
|
stepSummaryIndex := topRC.CurrentStepIndex
|
|
summaryFileCommand := path.Join("workflow", "step-summary-"+strconv.Itoa(stepSummaryIndex)+".md")
|
|
(*step.getEnv())["GITHUB_STEP_SUMMARY"] = path.Join(actPath, summaryFileCommand)
|
|
aliasGiteaEnv(*step.getEnv())
|
|
|
|
// Reset the per-phase file-command files. GITHUB_STEP_SUMMARY is intentionally
|
|
// excluded here and initialized below at most once per step so writes from later
|
|
// phases and from composite sub-steps accumulate instead of being truncated.
|
|
files := []*container.FileEntry{
|
|
{Name: outputFileCommand, Mode: 0o666},
|
|
{Name: stateFileCommand, Mode: 0o666},
|
|
{Name: pathFileCommand, Mode: 0o666},
|
|
{Name: envFileCommand, Mode: 0o666},
|
|
}
|
|
if topRC.summaryFileInitialized == nil {
|
|
topRC.summaryFileInitialized = map[int]bool{}
|
|
}
|
|
initialFiles := files
|
|
if !topRC.summaryFileInitialized[stepSummaryIndex] {
|
|
initialFiles = append([]*container.FileEntry{{Name: summaryFileCommand, Mode: 0o666}}, files...)
|
|
topRC.summaryFileInitialized[stepSummaryIndex] = true
|
|
}
|
|
fileCommandInitErr := rc.JobContainer.Copy(actPath, initialFiles...)(ctx)
|
|
if fileCommandInitErr != nil && len(initialFiles) > len(files) {
|
|
fileCommandInitErr = rc.JobContainer.Copy(actPath, files...)(ctx) // a broken summary must not block the resets
|
|
}
|
|
|
|
timeoutctx, cancelTimeOut := evaluateStepTimeout(ctx, step)
|
|
defer cancelTimeOut()
|
|
err = fileCommandInitErr
|
|
if err == nil {
|
|
err = rc.runWithCommands(timeoutctx, *step.getEnv(), executor)
|
|
}
|
|
fileCommandCtx := ctx
|
|
if ctx.Err() != nil { // an interrupted step still records its file commands, a live deadline stays in force
|
|
var cancelFileCommands context.CancelFunc
|
|
fileCommandCtx, cancelFileCommands = context.WithTimeout(context.WithoutCancel(ctx), jobCleanupTimeout)
|
|
defer cancelFileCommands()
|
|
}
|
|
if fileCommandInitErr == nil {
|
|
if fileErr := processRunnerEnvFileCommand(fileCommandCtx, envFileCommand, rc, rc.setEnvFile); fileErr != nil && err == nil {
|
|
err = fileErr
|
|
}
|
|
if fileErr := processRunnerEnvFileCommand(fileCommandCtx, stateFileCommand, rc, rc.saveState); fileErr != nil && err == nil {
|
|
err = fileErr
|
|
}
|
|
if fileErr := processRunnerEnvFileCommand(fileCommandCtx, outputFileCommand, rc, rc.setOutput); fileErr != nil && err == nil {
|
|
err = fileErr
|
|
}
|
|
if fileErr := rc.UpdateExtraPath(fileCommandCtx, path.Join(actPath, pathFileCommand)); fileErr != nil && err == nil {
|
|
err = fileErr
|
|
}
|
|
}
|
|
_ = rc.JobContainer.Copy(actPath, files...)(fileCommandCtx)
|
|
|
|
if err == nil {
|
|
logger.With("stepResult", stepResult.Conclusion).Infof("Success - %s %s", stage, stepString)
|
|
} else {
|
|
stepResult.Outcome = model.StepStatusFailure
|
|
|
|
continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step)
|
|
if parseErr != nil {
|
|
stepResult.Conclusion = model.StepStatusFailure
|
|
logger.With("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepString)
|
|
return parseErr
|
|
}
|
|
|
|
if continueOnError {
|
|
logger.Errorf("##[error]%s", EscapeCommandData(err.Error()))
|
|
logger.Infof("Failed but continue next step")
|
|
err = nil
|
|
stepResult.Conclusion = model.StepStatusSuccess
|
|
} else {
|
|
stepResult.Conclusion = model.StepStatusFailure
|
|
}
|
|
|
|
// Infof: Errorf entries are promoted to the user log by the reporter,
|
|
// which would duplicate the ##[error] annotation emitted elsewhere.
|
|
logger.With("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepString)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
func evaluateStepTimeout(ctx context.Context, step step) (context.Context, context.CancelFunc) {
|
|
if step.getStepModel().TimeoutMinutes == "" {
|
|
return ctx, func() {}
|
|
}
|
|
timeout, err := step.getRunContext().NewStepExpressionEvaluator(ctx, step).Interpolate(ctx, step.getStepModel().TimeoutMinutes)
|
|
if err != nil {
|
|
common.Logger(ctx).Errorf("An error occurred when attempting to determine the step timeout: %s", err)
|
|
} else if timeout != "" {
|
|
if timeOutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil && timeOutMinutes > 0 {
|
|
return context.WithTimeout(ctx, time.Duration(timeOutMinutes)*time.Minute)
|
|
}
|
|
}
|
|
return ctx, func() {}
|
|
}
|
|
|
|
func setupEnv(ctx context.Context, step step) error {
|
|
rc := step.getRunContext()
|
|
|
|
env := step.getEnv()
|
|
// container env is the image's baseline, which job env and $GITHUB_ENV override
|
|
mergeIntoMap(step, env, rc.containerSpec.Env, rc.GetEnv())
|
|
rc.withGithubEnv(ctx, getStepGithubContext(ctx, step), *env)
|
|
|
|
var err error
|
|
exprEval := rc.NewExpressionEvaluator(ctx)
|
|
stepEnv := step.getStepModel().Environment()
|
|
if envNode := step.getStepModel().Env; strings.Contains(envNode.Value, "${{") { // only a whole-map expression has a value
|
|
if err := model.DecodeEvaluated("step env", envNode, exprEval.shared(ctx).EvaluateYamlNode, &stepEnv); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
for k, v := range stepEnv {
|
|
if stepEnv[k], err = exprEval.Interpolate(ctx, v); err != nil {
|
|
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
|
}
|
|
}
|
|
}
|
|
// merge step env last, since it should not be overwritten
|
|
mergeIntoMap(step, step.getEnv(), stepEnv)
|
|
return nil
|
|
}
|
|
|
|
// setupInputs evaluates `with` once the condition passed, as GitHub does, so every consumer reads the evaluated With.
|
|
func setupInputs(ctx context.Context, step step) error {
|
|
stepModel := step.getStepModel()
|
|
if stepModel.RawWith.Kind == 0 {
|
|
return nil
|
|
}
|
|
if err := model.DecodeEvaluated("with", stepModel.RawWith, step.getRunContext().NewStepExpressionEvaluator(ctx, step).shared(ctx).EvaluateYamlNode, &stepModel.With); err != nil {
|
|
return err
|
|
}
|
|
mergeIntoMap(step, step.getEnv(), (&model.Step{With: stepModel.With}).GetEnv())
|
|
return nil
|
|
}
|
|
|
|
func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage) (bool, error) {
|
|
rc := step.getRunContext()
|
|
|
|
var defaultStatusCheck exprparser.DefaultStatusCheck
|
|
if stage == stepStagePost {
|
|
defaultStatusCheck = exprparser.DefaultStatusCheckAlways
|
|
} else {
|
|
defaultStatusCheck = exprparser.DefaultStatusCheckSuccess
|
|
}
|
|
|
|
// success() and failure() in a composite's own main steps read the composite's result, other stages the job's
|
|
jobContext := rc.getJobContext()
|
|
if rc.Parent != nil && stage == stepStageMain && jobContext.Status != "cancelled" {
|
|
jobContext.Status = rc.ownStatus()
|
|
}
|
|
runStep, err := EvalBool(ctx, rc.newStepExpressionEvaluator(ctx, step, rc.actionInputs, jobContext, rc.getGithubContext(ctx)), expr, defaultStatusCheck)
|
|
if err != nil {
|
|
return false, fmt.Errorf("if-expression %q evaluation failed: %s", expr, err)
|
|
}
|
|
|
|
return runStep, nil
|
|
}
|
|
|
|
func isContinueOnError(ctx context.Context, expr string, step step) (bool, error) {
|
|
// https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=true#L962
|
|
if strings.TrimSpace(expr) == "" {
|
|
return false, nil
|
|
}
|
|
|
|
rc := step.getRunContext()
|
|
|
|
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
|
|
if err != nil {
|
|
return false, fmt.Errorf("continue-on-error expression %q evaluation failed: %s", expr, err)
|
|
}
|
|
|
|
return continueOnError, nil
|
|
}
|
|
|
|
func mergeIntoMap(step step, target *map[string]string, maps ...map[string]string) {
|
|
if rc := step.getRunContext(); rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive() {
|
|
mergeIntoMapCaseInsensitive(*target, maps...)
|
|
} else {
|
|
mergeIntoMapCaseSensitive(*target, maps...)
|
|
}
|
|
}
|
|
|
|
func mergeIntoMapCaseSensitive(target map[string]string, maps ...map[string]string) {
|
|
for _, m := range maps {
|
|
maps0.Copy(target, m)
|
|
}
|
|
}
|
|
|
|
func mergeIntoMapCaseInsensitive(target map[string]string, maps ...map[string]string) {
|
|
foldKeys := make(map[string]string, len(target))
|
|
for k := range target {
|
|
foldKeys[strings.ToLower(k)] = k
|
|
}
|
|
toKey := func(s string) string {
|
|
foldKey := strings.ToLower(s)
|
|
if k, ok := foldKeys[foldKey]; ok {
|
|
return k
|
|
}
|
|
foldKeys[foldKey] = s
|
|
return s
|
|
}
|
|
for _, m := range maps {
|
|
for k, v := range m {
|
|
target[toKey(k)] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
func symlinkJoin(filename, sym, parent string) (string, error) {
|
|
dest := path.Join(path.Dir(filename), sym)
|
|
prefix := path.Clean(parent) + "/"
|
|
if strings.HasPrefix(dest, prefix) || prefix == "./" {
|
|
return dest, nil
|
|
}
|
|
return "", fmt.Errorf("symlink tries to access file '%s' outside of '%s'", strings.ReplaceAll(dest, "'", "''"), strings.ReplaceAll(parent, "'", "''"))
|
|
}
|