mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-07 09:24:22 +02:00
Every `${{ }}` part was spliced as raw text into a synthesized `format('...', <raw>)` call and re-parsed, so unbalanced parentheses restructured the whole expression:
```yaml
run: echo ${{ 1) && (2 }} # panicked with "did not evaluate to a string"
if: ${{ 1 }} ${{ 0) && (0 }} # silently skipped the step
```
One scanner shaped like GitHub's template reader now splits every value, and each part is evaluated on its own, so nothing builds an expression out of text.
An empty input to `Evaluate` asked `success()` whatever the caller requested, so a post step running under `always()` asked the wrong question.
Same fix as https://github.com/go-gitea/gitea/pull/38754 on the Gitea side.
Written by Claude Opus 5.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1146
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
627 lines
17 KiB
Go
627 lines
17 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 (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"path"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
"gitea.com/gitea/runner/act/container"
|
|
"gitea.com/gitea/runner/act/exprparser"
|
|
"gitea.com/gitea/runner/act/model"
|
|
|
|
_ "embed"
|
|
|
|
"github.com/rhysd/actionlint"
|
|
"go.yaml.in/yaml/v4"
|
|
)
|
|
|
|
// ExpressionEvaluator is the interface for evaluating expressions
|
|
type ExpressionEvaluator interface {
|
|
evaluate(context.Context, string, exprparser.DefaultStatusCheck) (any, error)
|
|
interpolate(context.Context, string) (string, error)
|
|
EvaluateYamlNode(context.Context, *yaml.Node) error
|
|
Interpolate(context.Context, string) string
|
|
}
|
|
|
|
// NewExpressionEvaluator creates a new evaluator
|
|
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator {
|
|
return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
|
|
}
|
|
|
|
func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map[string]string) ExpressionEvaluator {
|
|
var workflowCallResult map[string]*model.WorkflowCallResult
|
|
|
|
// todo: cleanup EvaluationEnvironment creation
|
|
using := make(map[string]exprparser.Needs)
|
|
strategy := make(map[string]any)
|
|
if rc.Run != nil {
|
|
job := rc.Run.Job()
|
|
if job != nil && job.Strategy != nil {
|
|
strategy["fail-fast"] = job.Strategy.FailFast
|
|
strategy["max-parallel"] = job.Strategy.MaxParallel
|
|
}
|
|
|
|
jobs := rc.Run.Workflow.Jobs
|
|
jobNeeds := rc.Run.Job().Needs()
|
|
|
|
for _, needs := range jobNeeds {
|
|
using[needs] = exprparser.Needs{
|
|
Outputs: jobs[needs].Outputs,
|
|
Result: jobs[needs].NeedsResult(),
|
|
}
|
|
}
|
|
|
|
// only setup jobs context in case of workflow_call
|
|
// and existing expression evaluator (this means, jobs are at
|
|
// least ready to run)
|
|
if rc.caller != nil && rc.ExprEval != nil {
|
|
workflowCallResult = map[string]*model.WorkflowCallResult{}
|
|
|
|
for jobName, job := range jobs {
|
|
result := model.WorkflowCallResult{
|
|
Outputs: map[string]string{},
|
|
}
|
|
maps.Copy(result.Outputs, job.Outputs)
|
|
workflowCallResult[jobName] = &result
|
|
}
|
|
}
|
|
}
|
|
|
|
ghc := rc.getGithubContext(ctx)
|
|
inputs := getEvaluatorInputs(ctx, rc, nil, ghc)
|
|
|
|
ee := &exprparser.EvaluationEnvironment{
|
|
Github: ghc,
|
|
Env: env,
|
|
Job: rc.getJobContext(),
|
|
Jobs: &workflowCallResult,
|
|
// todo: should be unavailable
|
|
// but required to interpolate/evaluate the step outputs on the job
|
|
Steps: rc.getStepsContext(),
|
|
Secrets: getWorkflowSecrets(ctx, rc),
|
|
Vars: getWorkflowVars(ctx, rc),
|
|
Strategy: strategy,
|
|
Matrix: rc.Matrix,
|
|
Needs: using,
|
|
Inputs: inputs,
|
|
HashFiles: getHashFilesFunction(ctx, rc),
|
|
}
|
|
ee.Runner = rc.getRunnerContext(ctx)
|
|
return expressionEvaluator{
|
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
|
Run: rc.Run,
|
|
WorkingDir: rc.Config.Workdir,
|
|
Context: "job",
|
|
}),
|
|
}
|
|
}
|
|
|
|
//go:embed hashfiles/index.js
|
|
var hashfiles string
|
|
|
|
// NewStepExpressionEvaluator creates a new evaluator
|
|
func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) ExpressionEvaluator {
|
|
// todo: cleanup EvaluationEnvironment creation
|
|
job := rc.Run.Job()
|
|
strategy := make(map[string]any)
|
|
if job.Strategy != nil {
|
|
strategy["fail-fast"] = job.Strategy.FailFast
|
|
strategy["max-parallel"] = job.Strategy.MaxParallel
|
|
}
|
|
|
|
jobs := rc.Run.Workflow.Jobs
|
|
jobNeeds := rc.Run.Job().Needs()
|
|
|
|
using := make(map[string]exprparser.Needs)
|
|
for _, needs := range jobNeeds {
|
|
using[needs] = exprparser.Needs{
|
|
Outputs: jobs[needs].Outputs,
|
|
Result: jobs[needs].NeedsResult(),
|
|
}
|
|
}
|
|
|
|
ghc := rc.getGithubContext(ctx)
|
|
inputs := getEvaluatorInputs(ctx, rc, step, ghc)
|
|
|
|
ee := &exprparser.EvaluationEnvironment{
|
|
Github: step.getGithubContext(ctx),
|
|
Env: *step.getEnv(),
|
|
Job: rc.getJobContext(),
|
|
Steps: rc.getStepsContext(),
|
|
Secrets: getWorkflowSecrets(ctx, rc),
|
|
Vars: getWorkflowVars(ctx, rc),
|
|
Strategy: strategy,
|
|
Matrix: rc.Matrix,
|
|
Needs: using,
|
|
// todo: should be unavailable
|
|
// but required to interpolate/evaluate the inputs in actions/composite
|
|
Inputs: inputs,
|
|
HashFiles: getHashFilesFunction(ctx, rc),
|
|
}
|
|
ee.Runner = rc.getRunnerContext(ctx)
|
|
return expressionEvaluator{
|
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
|
Run: rc.Run,
|
|
WorkingDir: rc.Config.Workdir,
|
|
Context: "step",
|
|
}),
|
|
}
|
|
}
|
|
|
|
func getHashFilesFunction(ctx context.Context, rc *RunContext) func(v []reflect.Value) (any, error) {
|
|
hashFiles := func(v []reflect.Value) (any, error) {
|
|
if rc.JobContainer != nil {
|
|
timeed, cancel := context.WithTimeout(ctx, time.Minute)
|
|
defer cancel()
|
|
name := "workflow/hashfiles/index.js"
|
|
hout := &bytes.Buffer{}
|
|
herr := &bytes.Buffer{}
|
|
patterns := []string{}
|
|
followSymlink := false
|
|
|
|
for i, p := range v {
|
|
s := p.String()
|
|
if i == 0 {
|
|
if strings.HasPrefix(s, "--") {
|
|
if strings.EqualFold(s, "--follow-symbolic-links") {
|
|
followSymlink = true
|
|
continue
|
|
}
|
|
return "", fmt.Errorf("Invalid glob option %s, available option: '--follow-symbolic-links'", s)
|
|
}
|
|
}
|
|
patterns = append(patterns, s)
|
|
}
|
|
env := map[string]string{}
|
|
maps.Copy(env, rc.Env)
|
|
env["patterns"] = strings.Join(patterns, "\n")
|
|
if followSymlink {
|
|
env["followSymbolicLinks"] = "true"
|
|
}
|
|
|
|
stdout, stderr := rc.JobContainer.ReplaceLogWriter(hout, herr)
|
|
_ = rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{
|
|
Name: name,
|
|
Mode: 0o644,
|
|
Body: hashfiles,
|
|
}).
|
|
Then(rc.execJobContainer([]string{"node", path.Join(rc.JobContainer.GetActPath(), name)},
|
|
env, "", "")).
|
|
Finally(func(context.Context) error {
|
|
rc.JobContainer.ReplaceLogWriter(stdout, stderr)
|
|
return nil
|
|
})(timeed)
|
|
output := hout.String() + "\n" + herr.String()
|
|
guard := "__OUTPUT__"
|
|
outstart := strings.Index(output, guard)
|
|
if outstart != -1 {
|
|
outstart += len(guard)
|
|
outend := strings.Index(output[outstart:], guard)
|
|
if outend != -1 {
|
|
return output[outstart : outstart+outend], nil
|
|
}
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
return hashFiles
|
|
}
|
|
|
|
type expressionEvaluator struct {
|
|
interpreter exprparser.Interpreter
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluate(ctx context.Context, in string, defaultStatusCheck exprparser.DefaultStatusCheck) (any, error) {
|
|
logger := common.Logger(ctx)
|
|
logger.Debugf("evaluating expression '%s'", in)
|
|
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
|
|
|
// evaluated is an any: %t renders everything but a bool as "%!t(string=...)"
|
|
printable := regexp.MustCompile(`::add-mask::.*`).ReplaceAllString(fmt.Sprintf("%v", evaluated), "::add-mask::***)")
|
|
logger.Debugf("expression '%s' evaluated to '%s'", in, printable)
|
|
|
|
return evaluated, err
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateScalarYamlNode(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
|
|
var in string
|
|
if err := node.Decode(&in); err != nil {
|
|
return nil, err
|
|
}
|
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
|
return nil, nil //nolint:nilnil // pre-existing issue from nektos/act
|
|
}
|
|
res, err := ee.evaluateScalar(ctx, in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret := &yaml.Node{}
|
|
if err := ret.Encode(res); err != nil {
|
|
return nil, err
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateMappingYamlNode(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
|
|
var ret *yaml.Node
|
|
// GitHub has this undocumented feature to merge maps, called insert directive
|
|
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
|
for i := 0; i < len(node.Content)/2; i++ {
|
|
changed := func() error {
|
|
if ret == nil {
|
|
ret = &yaml.Node{}
|
|
if err := ret.Encode(node); err != nil {
|
|
return err
|
|
}
|
|
ret.Content = ret.Content[:i*2]
|
|
}
|
|
return nil
|
|
}
|
|
k := node.Content[i*2]
|
|
v := node.Content[i*2+1]
|
|
ev, err := ee.evaluateYamlNodeInternal(ctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ev != nil {
|
|
if err := changed(); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
ev = v
|
|
}
|
|
var sk string
|
|
// Merge the nested map of the insert directive
|
|
if k.Decode(&sk) == nil && insertDirective.MatchString(sk) {
|
|
if ev.Kind != yaml.MappingNode {
|
|
return nil, fmt.Errorf("failed to insert node %v into mapping %v unexpected type %v expected MappingNode", ev, node, ev.Kind)
|
|
}
|
|
if err := changed(); err != nil {
|
|
return nil, err
|
|
}
|
|
ret.Content = append(ret.Content, ev.Content...)
|
|
} else {
|
|
ek, err := ee.evaluateYamlNodeInternal(ctx, k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ek != nil {
|
|
if err := changed(); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
ek = k
|
|
}
|
|
if ret != nil {
|
|
ret.Content = append(ret.Content, ek, ev)
|
|
}
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateSequenceYamlNode(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
|
|
var ret *yaml.Node
|
|
for i := 0; i < len(node.Content); i++ {
|
|
v := node.Content[i]
|
|
// Preserve nested sequences
|
|
wasseq := v.Kind == yaml.SequenceNode
|
|
ev, err := ee.evaluateYamlNodeInternal(ctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ev != nil {
|
|
if ret == nil {
|
|
ret = &yaml.Node{}
|
|
if err := ret.Encode(node); err != nil {
|
|
return nil, err
|
|
}
|
|
ret.Content = ret.Content[:i]
|
|
}
|
|
// GitHub has this undocumented feature to merge sequences / arrays
|
|
// We have a nested sequence via evaluation, merge the arrays
|
|
if ev.Kind == yaml.SequenceNode && !wasseq {
|
|
ret.Content = append(ret.Content, ev.Content...)
|
|
} else {
|
|
ret.Content = append(ret.Content, ev)
|
|
}
|
|
} else if ret != nil {
|
|
ret.Content = append(ret.Content, v)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (ee expressionEvaluator) evaluateYamlNodeInternal(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
|
|
switch node.Kind {
|
|
case yaml.ScalarNode:
|
|
return ee.evaluateScalarYamlNode(ctx, node)
|
|
case yaml.MappingNode:
|
|
return ee.evaluateMappingYamlNode(ctx, node)
|
|
case yaml.SequenceNode:
|
|
return ee.evaluateSequenceYamlNode(ctx, node)
|
|
default:
|
|
return nil, nil //nolint:nilnil // pre-existing issue from nektos/act
|
|
}
|
|
}
|
|
|
|
func (ee expressionEvaluator) EvaluateYamlNode(ctx context.Context, node *yaml.Node) error {
|
|
ret, err := ee.evaluateYamlNodeInternal(ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ret != nil {
|
|
return ret.Decode(node)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ee expressionEvaluator) Interpolate(ctx context.Context, in string) string {
|
|
out, err := ee.interpolate(ctx, in)
|
|
if err != nil {
|
|
common.Logger(ctx).Errorf("Unable to interpolate expression '%s': %s", in, err)
|
|
return ""
|
|
}
|
|
return out
|
|
}
|
|
|
|
// interpolate evaluates every part on its own, so a malformed one cannot restructure its neighbours
|
|
func (ee expressionEvaluator) interpolate(ctx context.Context, in string) (string, error) {
|
|
parts, err := splitSubExpressions(in)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(parts) == 1 && !parts[0].isExpr {
|
|
return in, nil
|
|
}
|
|
var out strings.Builder
|
|
out.Grow(len(in))
|
|
for _, part := range parts {
|
|
if !part.isExpr {
|
|
out.WriteString(part.text)
|
|
continue
|
|
}
|
|
evaluated, err := ee.evaluate(ctx, part.text, exprparser.DefaultStatusCheckNone)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
out.WriteString(exprparser.CoerceToString(evaluated))
|
|
}
|
|
return out.String(), nil
|
|
}
|
|
|
|
// evaluateScalar keeps the type of a lone expression, so `${{ fromJSON('[1,2]') }}` stays an array
|
|
func (ee expressionEvaluator) evaluateScalar(ctx context.Context, in string) (any, error) {
|
|
parts, err := splitSubExpressions(in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(parts) == 1 && parts[0].isExpr {
|
|
return ee.evaluate(ctx, parts[0].text, exprparser.DefaultStatusCheckNone)
|
|
}
|
|
return ee.interpolate(ctx, in)
|
|
}
|
|
|
|
// EvalBool evaluates an expression against given evaluator. An `if:` is an expression even without
|
|
// `${{ }}`, while literal text around one makes the whole value a string.
|
|
func EvalBool(ctx context.Context, evaluator ExpressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) {
|
|
parts, err := splitSubExpressions(expr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if len(parts) == 1 {
|
|
evaluated, err := evaluator.evaluate(ctx, parts[0].text, defaultStatusCheck)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return exprparser.IsTruthy(evaluated), nil
|
|
}
|
|
|
|
// mixed content is a string, so the status check applies to it separately
|
|
if defaultStatusCheck != exprparser.DefaultStatusCheckNone && !callsStatusFunction(parts) {
|
|
status, err := evaluator.evaluate(ctx, "", defaultStatusCheck)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !exprparser.IsTruthy(status) {
|
|
return false, nil
|
|
}
|
|
}
|
|
interpolated, err := evaluator.interpolate(ctx, expr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return exprparser.IsTruthy(interpolated), nil
|
|
}
|
|
|
|
// callsStatusFunction reports whether any part calls a status function. A part that does not parse
|
|
// counts as one, so the evaluation reports it against the real values.
|
|
func callsStatusFunction(parts []exprPart) bool {
|
|
for _, part := range parts {
|
|
if !part.isExpr {
|
|
continue
|
|
}
|
|
// The lexer needs the closing `}}` that the scanner strips.
|
|
exprNode, err := actionlint.NewExprParser().Parse(actionlint.NewExprLexer(part.text + "}}"))
|
|
if err != nil || exprparser.CallsStatusFunction(exprNode) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type exprPart struct {
|
|
text string
|
|
isExpr bool
|
|
}
|
|
|
|
// splitSubExpressions splits in the way GitHub's template reader does, leaving a value without a
|
|
// complete expression literal.
|
|
func splitSubExpressions(in string) ([]exprPart, error) {
|
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
|
return []exprPart{{text: in}}, nil
|
|
}
|
|
|
|
parts := make([]exprPart, 0, 2*strings.Count(in, "${{")+1)
|
|
for {
|
|
start := strings.Index(in, "${{")
|
|
if start < 0 {
|
|
if in != "" {
|
|
parts = append(parts, exprPart{text: in})
|
|
}
|
|
return parts, nil
|
|
}
|
|
if start > 0 {
|
|
parts = append(parts, exprPart{text: in[:start]})
|
|
}
|
|
rest := in[start+len("${{"):]
|
|
end := indexExprEnd(rest)
|
|
if end < 0 {
|
|
return nil, errors.New("unclosed expression")
|
|
}
|
|
parts = append(parts, exprPart{text: strings.TrimSpace(rest[:end]), isExpr: true})
|
|
in = rest[end+len("}}"):]
|
|
}
|
|
}
|
|
|
|
// indexExprEnd returns the offset of the `}}` ending an expression, or -1. A quote toggles string
|
|
// state, so a `}}` inside a string does not end it.
|
|
func indexExprEnd(in string) int {
|
|
inString := false
|
|
for i := range len(in) {
|
|
switch {
|
|
case in[i] == '\'':
|
|
inString = !inString
|
|
case !inString && in[i] == '}' && i+1 < len(in) && in[i+1] == '}':
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]any {
|
|
inputs := map[string]any{}
|
|
|
|
setupWorkflowInputs(ctx, &inputs, rc)
|
|
|
|
var env map[string]string
|
|
if step != nil {
|
|
env = *step.getEnv()
|
|
} else {
|
|
env = rc.GetEnv()
|
|
}
|
|
|
|
for k, v := range env {
|
|
if after, ok := strings.CutPrefix(k, "INPUT_"); ok {
|
|
inputs[strings.ToLower(after)] = v
|
|
}
|
|
}
|
|
|
|
if ghc.EventName == "workflow_dispatch" {
|
|
config := rc.Run.Workflow.WorkflowDispatchConfig()
|
|
if config != nil && config.Inputs != nil {
|
|
for k, v := range config.Inputs {
|
|
value := nestedMapLookup(ghc.Event, "inputs", k)
|
|
if value == nil {
|
|
value = v.Default
|
|
}
|
|
inputs[k] = coerceInputValue(value, v.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
if ghc.EventName == "workflow_call" {
|
|
config := rc.Run.Workflow.WorkflowCallConfig()
|
|
if config != nil && config.Inputs != nil {
|
|
for k, v := range config.Inputs {
|
|
value := nestedMapLookup(ghc.Event, "inputs", k)
|
|
if value == nil {
|
|
value = v.Default
|
|
}
|
|
inputs[k] = coerceInputValue(value, v.Type)
|
|
}
|
|
}
|
|
}
|
|
return inputs
|
|
}
|
|
|
|
// coerceInputValue converts an input value to the type declared by the workflow.
|
|
// The event payload carries natively typed JSON values on newer Gitea versions,
|
|
// while defaults and older servers provide strings.
|
|
func coerceInputValue(value any, inputType string) any {
|
|
if inputType != "boolean" {
|
|
return value
|
|
}
|
|
if b, ok := value.(bool); ok {
|
|
return b
|
|
}
|
|
return value == "true"
|
|
}
|
|
|
|
func setupWorkflowInputs(ctx context.Context, inputs *map[string]any, rc *RunContext) {
|
|
if rc.caller != nil {
|
|
config := rc.Run.Workflow.WorkflowCallConfig()
|
|
|
|
for name, input := range config.Inputs {
|
|
value := rc.caller.runContext.Run.Job().With[name]
|
|
if value != nil {
|
|
if str, ok := value.(string); ok {
|
|
// evaluate using the calling RunContext (outside)
|
|
value = rc.caller.runContext.ExprEval.Interpolate(ctx, str)
|
|
}
|
|
}
|
|
|
|
if value == nil && config != nil && config.Inputs != nil {
|
|
value = input.Default
|
|
if rc.ExprEval != nil {
|
|
if str, ok := value.(string); ok {
|
|
// evaluate using the called RunContext (inside)
|
|
value = rc.ExprEval.Interpolate(ctx, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
(*inputs)[name] = coerceInputValue(value, input.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getWorkflowSecrets(ctx context.Context, rc *RunContext) map[string]string {
|
|
if rc.caller != nil {
|
|
job := rc.caller.runContext.Run.Job()
|
|
secrets := job.Secrets()
|
|
|
|
if secrets == nil && job.InheritSecrets() {
|
|
secrets = rc.caller.runContext.Config.Secrets
|
|
}
|
|
|
|
// Interpolate into a new map. secrets may be the shared Config.Secrets (or the job's
|
|
// map), which other parallel jobs read concurrently (e.g. log masking), so mutating it
|
|
// in place is a data race.
|
|
interpolated := make(map[string]string, len(secrets))
|
|
for k, v := range secrets {
|
|
interpolated[k] = rc.caller.runContext.ExprEval.Interpolate(ctx, v)
|
|
}
|
|
|
|
return interpolated
|
|
}
|
|
|
|
return rc.Config.Secrets
|
|
}
|
|
|
|
func getWorkflowVars(_ context.Context, rc *RunContext) map[string]string {
|
|
return rc.Config.Vars
|
|
}
|