mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-04 07:33:47 +00:00
feat: update to actionslib v1.2.0, support self: (#1233)
Update to [actionslib v1.2.0](https://gitea.com/gitea/actionslib/releases/tag/v1.2.0) and move logic to it. 1. `self:{owner}/{repo}[/{path}]@{ref}` in step `uses` resolves on the runner's Gitea instance, for https://github.com/go-gitea/gitea/issues/38990. 2. `strategy.job-index` and `strategy.job-total` come from Gitea when it sends them. 3. Container, services and `with` are evaluated once instead of on every use. 4. Fixes a flaky clone-lock test. Gitea PR: https://github.com/go-gitea/gitea/pull/39358 Docs PR: https://gitea.com/gitea/docs/pulls/553 --------- Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1233 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
This commit is contained in:
1 parent
ae9ce10f71
commit
e0d00e543d
24 files changed
+454
-721
No files matched your search
+14
-36
@@ -13,6 +13,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -353,12 +354,7 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
||||
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
||||
}
|
||||
}
|
||||
eval := rc.NewActionInputsExpressionEvaluator(ctx, step)
|
||||
args, err := eval.Interpolate(ctx, step.getStepModel().With["args"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate with.args: %w", err)
|
||||
}
|
||||
cmd, err := shellquote.Split(args)
|
||||
cmd, err := shellquote.Split(step.getStepModel().With["args"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -375,14 +371,11 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
||||
}
|
||||
}
|
||||
}
|
||||
entrypoint, err := dockerEntrypoint(ctx, step, eval, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stepContainer, err := newStepContainer(ctx, step, image, cmd, entrypoint, rc.Config.ContainerOptions)
|
||||
entrypoint, err := dockerEntrypoint(step, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint, rc.Config.ContainerOptions)
|
||||
return common.NewPipelineExecutor(
|
||||
prepImage,
|
||||
stepContainer.Pull(forcePull),
|
||||
@@ -394,7 +387,7 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
||||
|
||||
// dockerEntrypoint returns the entrypoint the action's image runs with for the given
|
||||
// stage. Only the main stage honours the `entrypoint` input.
|
||||
func dockerEntrypoint(ctx context.Context, step actionStep, eval *expressionEvaluator, stage stepStage) ([]string, error) {
|
||||
func dockerEntrypoint(step actionStep, stage stepStage) ([]string, error) {
|
||||
runs := step.getActionModel().Runs
|
||||
|
||||
var entrypoint string
|
||||
@@ -406,11 +399,7 @@ func dockerEntrypoint(ctx context.Context, step actionStep, eval *expressionEval
|
||||
default:
|
||||
entrypoint = runs.Entrypoint
|
||||
if entrypoint == "" {
|
||||
withEntrypoint, err := eval.Interpolate(ctx, step.getStepModel().With["entrypoint"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to interpolate with.entrypoint: %w", err)
|
||||
}
|
||||
if fields := strings.Fields(withEntrypoint); len(fields) > 0 {
|
||||
if fields := strings.Fields(step.getStepModel().With["entrypoint"]); len(fields) > 0 {
|
||||
return fields, nil
|
||||
}
|
||||
}
|
||||
@@ -425,7 +414,6 @@ func dockerEntrypoint(ctx context.Context, step actionStep, eval *expressionEval
|
||||
// evalDockerEnv returns an evaluator bound to the environment it installed.
|
||||
func evalDockerEnv(ctx context.Context, step step, action *model.Action) (*expressionEvaluator, error) {
|
||||
rc := step.getRunContext()
|
||||
stepModel := step.getStepModel()
|
||||
|
||||
var err error
|
||||
inputs := make(map[string]string)
|
||||
@@ -436,27 +424,20 @@ func evalDockerEnv(ctx context.Context, step step, action *model.Action) (*expre
|
||||
return nil, fmt.Errorf("unable to interpolate the default of input %s: %w", k, err)
|
||||
}
|
||||
}
|
||||
for k, v := range stepModel.With {
|
||||
if inputs[k], err = eval.Interpolate(ctx, v); err != nil {
|
||||
return nil, fmt.Errorf("unable to interpolate with.%s: %w", k, err)
|
||||
}
|
||||
}
|
||||
mergeIntoMap(step, step.getEnv(), inputs)
|
||||
|
||||
env := make(map[string]string, len(action.Runs.Env)+len(*step.getEnv()))
|
||||
mergeIntoMap(step, &env, action.Runs.Env, *step.getEnv())
|
||||
*step.getEnv() = env
|
||||
mergeIntoMap(step, step.getEnv(), inputs, step.getStepModel().With)
|
||||
|
||||
ee := rc.NewActionInputsExpressionEvaluator(ctx, step)
|
||||
for k, v := range *step.getEnv() {
|
||||
if (*step.getEnv())[k], err = ee.Interpolate(ctx, v); err != nil {
|
||||
runsEnv := make(map[string]string, len(action.Runs.Env))
|
||||
for k, v := range action.Runs.Env {
|
||||
if runsEnv[k], err = ee.Interpolate(ctx, v); err != nil {
|
||||
return nil, fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
||||
}
|
||||
}
|
||||
mergeIntoMap(step, step.getEnv(), runsEnv, maps.Clone(*step.getEnv()))
|
||||
return ee, nil
|
||||
}
|
||||
|
||||
func newStepContainer(ctx context.Context, step step, image string, cmd, entrypoint []string, runnerOptions string) (container.Container, error) {
|
||||
func newStepContainer(ctx context.Context, step step, image string, cmd, entrypoint []string, runnerOptions string) container.Container {
|
||||
rc := step.getRunContext()
|
||||
logWriter := rc.commandLogWriter(ctx)
|
||||
envList := make([]string, 0)
|
||||
@@ -466,10 +447,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
||||
|
||||
envList = append(envList, rc.runnerEnv(ctx)...)
|
||||
|
||||
binds, mounts, err := rc.GetBindsAndMounts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
binds, mounts := rc.GetBindsAndMounts()
|
||||
networkMode := "container:" + rc.jobContainerName()
|
||||
if rc.IsHostEnv() {
|
||||
networkMode = "default"
|
||||
@@ -493,7 +471,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
||||
AutoRemove: true,
|
||||
ValidVolumes: rc.validVolumes(),
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
}), nil
|
||||
})
|
||||
}
|
||||
|
||||
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
|
||||
|
||||
@@ -104,6 +104,8 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action
|
||||
Parent: parent,
|
||||
EventJSON: parent.EventJSON,
|
||||
platformImage: parent.platformImage,
|
||||
jobIndex: parent.jobIndex,
|
||||
jobTotal: parent.jobTotal,
|
||||
}
|
||||
compositerc.setCompositeActionEnv(env)
|
||||
compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
|
||||
|
||||
@@ -12,18 +12,21 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func TestCompositeActionParity(t *testing.T) {
|
||||
t.Run("inherits contexts without leaking inputs", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
strategy := &model.Strategy{MaxParallelString: "3"}
|
||||
strategy := &model.Strategy{MaxParallelString: "3", RawMatrix: yaml.Node{Kind: yaml.MappingNode}}
|
||||
parent := &RunContext{
|
||||
Config: &Config{},
|
||||
Matrix: map[string]any{"os": "linux"},
|
||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Name: "workflow", Jobs: map[string]*model.Job{"job": {Strategy: strategy}}}},
|
||||
JobContainer: &jobContainerMock{},
|
||||
platformImage: "-self-hosted",
|
||||
jobIndex: 1,
|
||||
jobTotal: 4,
|
||||
}
|
||||
composite, err := newCompositeRunContext(ctx, parent, &stepActionRemote{
|
||||
Step: &model.Step{With: map[string]string{"SHARED": "outer"}},
|
||||
@@ -36,9 +39,9 @@ func TestCompositeActionParity(t *testing.T) {
|
||||
assert.Same(t, strategy, composite.Run.Job().Strategy)
|
||||
assert.True(t, composite.IsHostEnv())
|
||||
interpolated, err := composite.NewExpressionEvaluator(ctx).Interpolate(ctx,
|
||||
"${{ matrix.os }}|${{ strategy.max-parallel }}|${{ inputs.shared }}")
|
||||
"${{ matrix.os }}|${{ strategy.max-parallel }}|${{ strategy.job-index }}|${{ inputs.shared }}")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "linux|3|outer", interpolated)
|
||||
assert.Equal(t, "linux|3|1|outer", interpolated)
|
||||
assert.NotContains(t, composite.Env, "INPUT_SHARED")
|
||||
|
||||
nestedEnv := composite.GetEnv()
|
||||
|
||||
@@ -242,7 +242,7 @@ func TestActionRunner(t *testing.T) {
|
||||
|
||||
tt.step.getRunContext().JobContainer = cm
|
||||
|
||||
err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx)
|
||||
err := runActionImpl(tt.step, "dir", mustNewRemoteAction(t, "org/repo/path@ref"))(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
cm.AssertExpectations(t)
|
||||
@@ -316,7 +316,7 @@ func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
|
||||
step.On("getStepModel").Return(&model.Step{ID: "action"})
|
||||
step.On("getEnv").Return(&env)
|
||||
|
||||
_, _ = newStepContainer(ctx, step, "registry.example.com/action:tag", nil, nil, "")
|
||||
newStepContainer(ctx, step, "registry.example.com/action:tag", nil, nil, "")
|
||||
|
||||
// DOCKER_USERNAME/DOCKER_PASSWORD should not be injected as pull credentials for docker action containers.
|
||||
assert.Empty(t, captured.Username)
|
||||
@@ -461,18 +461,11 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
||||
case <-innerEntered:
|
||||
case err := <-done:
|
||||
t.Fatalf("execAsDocker returned without entering inner: %v", err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("inner build executor not entered after lock released")
|
||||
}
|
||||
|
||||
cancel()
|
||||
releaseOnce()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("execAsDocker did not return after inner was released and ctx was canceled")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestDockerActionImageTag(t *testing.T) {
|
||||
|
||||
+38
-116
@@ -13,7 +13,6 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -27,29 +26,6 @@ import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// setStrategyContext leaves `max-parallel` unset when the job declares none, as GitHub does.
|
||||
func setStrategyContext(strategy map[string]any, jobStrategy *model.Strategy) {
|
||||
strategy["fail-fast"] = jobStrategy.GetFailFast()
|
||||
if limit, declared, err := jobStrategy.ParseMaxParallel(); declared && err == nil {
|
||||
strategy["max-parallel"] = limit
|
||||
}
|
||||
}
|
||||
|
||||
// decodeDeferred decodes a whole-value `${{ }}` parked in a Raw* field, evaluating a clone since the node is shared across matrix combinations.
|
||||
func decodeDeferred[T any](ctx context.Context, eval *expressionEvaluator, name string, raw yaml.Node, out *T) error {
|
||||
if raw.Kind != yaml.ScalarNode {
|
||||
return nil
|
||||
}
|
||||
node := model.CloneYamlNode(raw)
|
||||
if err := eval.EvaluateYamlNode(ctx, &node); err != nil {
|
||||
return fmt.Errorf("unable to evaluate %s: %w", name, err)
|
||||
}
|
||||
if err := node.Decode(out); err != nil {
|
||||
return fmt.Errorf("unable to decode %s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewExpressionEvaluator creates a new evaluator
|
||||
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) *ExpressionEvaluator {
|
||||
return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
|
||||
@@ -59,23 +35,9 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
||||
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 {
|
||||
setStrategyContext(strategy, job.Strategy)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
strategy = exprparser.StrategyContext(rc.Run.Job().Strategy, rc.jobIndex, rc.jobTotal)
|
||||
|
||||
// only setup jobs context in case of workflow_call
|
||||
// and existing expression evaluator (this means, jobs are at
|
||||
@@ -83,7 +45,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
||||
if rc.caller != nil && rc.ExprEval != nil {
|
||||
workflowCallResult = map[string]*model.WorkflowCallResult{}
|
||||
|
||||
for jobName, job := range jobs {
|
||||
for jobName, job := range rc.Run.Workflow.Jobs {
|
||||
result := model.WorkflowCallResult{
|
||||
Outputs: map[string]string{},
|
||||
}
|
||||
@@ -108,7 +70,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
||||
Vars: getWorkflowVars(ctx, rc),
|
||||
Strategy: strategy,
|
||||
Matrix: rc.Matrix,
|
||||
Needs: using,
|
||||
Needs: exprparser.NeedsContext(rc.Run),
|
||||
Inputs: inputs,
|
||||
HashFiles: getHashFilesFunction(ctx, rc),
|
||||
}
|
||||
@@ -137,23 +99,6 @@ func (rc *RunContext) NewActionInputsExpressionEvaluator(ctx context.Context, st
|
||||
|
||||
func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, stepInputs map[string]any, jobContext *model.JobContext) *ExpressionEvaluator {
|
||||
// todo: cleanup EvaluationEnvironment creation
|
||||
job := rc.Run.Job()
|
||||
strategy := make(map[string]any)
|
||||
if job.Strategy != nil {
|
||||
setStrategyContext(strategy, job.Strategy)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
ee := &exprparser.EvaluationEnvironment{
|
||||
Github: step.getGithubContext(ctx),
|
||||
Env: *step.getEnv(),
|
||||
@@ -161,9 +106,9 @@ func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step,
|
||||
Steps: rc.getStepsContext(),
|
||||
Secrets: getWorkflowSecrets(rc),
|
||||
Vars: getWorkflowVars(ctx, rc),
|
||||
Strategy: strategy,
|
||||
Strategy: exprparser.StrategyContext(rc.Run.Job().Strategy, rc.jobIndex, rc.jobTotal),
|
||||
Matrix: rc.Matrix,
|
||||
Needs: using,
|
||||
Needs: exprparser.NeedsContext(rc.Run),
|
||||
// todo: should be unavailable
|
||||
// but required to interpolate/evaluate the inputs in actions/composite
|
||||
Inputs: getEvaluatorInputs(rc, stepInputs, rc.getGithubContext(ctx)),
|
||||
@@ -306,45 +251,30 @@ func getEvaluatorInputs(rc *RunContext, stepInputs map[string]any, ghc *model.Gi
|
||||
maps.Copy(inputs, rc.workflowCallInputs)
|
||||
maps.Copy(inputs, stepInputs)
|
||||
|
||||
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
|
||||
switch ghc.EventName {
|
||||
case "workflow_dispatch":
|
||||
if config := rc.Run.Workflow.WorkflowDispatchConfig(); config != nil {
|
||||
for name, input := range config.Inputs {
|
||||
if input.Type == "number" {
|
||||
input.Type = "string" // boolean is the only typed dispatch input, as in Gitea
|
||||
}
|
||||
inputs[k] = coerceInputValue(value, v.Type)
|
||||
setEvaluatorInput(inputs, ghc, name, model.WorkflowCallInput{Type: input.Type, Default: input.Default})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
case "workflow_call":
|
||||
for name, input := range rc.Run.Workflow.WorkflowCallConfig().Inputs {
|
||||
setEvaluatorInput(inputs, ghc, name, input)
|
||||
}
|
||||
}
|
||||
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
|
||||
func setEvaluatorInput(inputs map[string]any, ghc *model.GithubContext, name string, input model.WorkflowCallInput) {
|
||||
value := model.NestedMapLookup(ghc.Event, "inputs", name)
|
||||
if value == nil {
|
||||
value, _ = input.DefaultValue()
|
||||
}
|
||||
if b, ok := value.(bool); ok {
|
||||
return b
|
||||
}
|
||||
return value == "true"
|
||||
inputs[name] = model.CoerceInputValue(value, input.Type)
|
||||
}
|
||||
|
||||
// resolveWorkflowCall evaluates the caller's with: and secrets: once, as the server does before dispatching a called workflow.
|
||||
@@ -352,40 +282,32 @@ func (rc *RunContext) resolveWorkflowCall(ctx context.Context) error {
|
||||
if rc.caller == nil {
|
||||
return nil
|
||||
}
|
||||
logger := common.Logger(ctx)
|
||||
callerJob := rc.caller.runContext.Run.Job()
|
||||
callerEval := rc.caller.runContext.ExprEval
|
||||
callerWith := callerJob.With
|
||||
if err := decodeDeferred(ctx, callerEval, "workflow inputs", callerJob.RawWith, &callerWith); err != nil {
|
||||
callerEval := rc.caller.runContext.ExprEval.shared(ctx)
|
||||
var callerWith map[string]any
|
||||
if err := model.DecodeEvaluated("workflow inputs", callerJob.RawWith, callerEval.EvaluateYamlNode, &callerWith); err != nil {
|
||||
return err
|
||||
}
|
||||
calleeEval := sync.OnceValue(func() *expressionEvaluator { return rc.NewExpressionEvaluator(ctx) })
|
||||
if !rc.Run.Workflow.IsWorkflowCall() {
|
||||
logger.Warn("workflow_call key is not defined in the referenced workflow")
|
||||
}
|
||||
config := rc.Run.Workflow.WorkflowCallConfig()
|
||||
|
||||
rc.workflowCallInputs = make(map[string]any, len(config.Inputs))
|
||||
for name, input := range config.Inputs {
|
||||
value, eval, label := callerWith[name], callerEval, "input"
|
||||
if value == nil {
|
||||
value, eval, label = input.Default, calleeEval(), "the default of input"
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
var err error
|
||||
if value, err = eval.Interpolate(ctx, str); err != nil {
|
||||
return fmt.Errorf("unable to interpolate %s %s: %w", label, name, err)
|
||||
}
|
||||
}
|
||||
rc.workflowCallInputs[name] = coerceInputValue(value, input.Type)
|
||||
var err error
|
||||
if rc.workflowCallInputs, err = callerEval.ResolveWorkflowCallInputs(config, callerWith); err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
|
||||
if callerJob.InheritSecrets() {
|
||||
rc.workflowCallSecrets = getWorkflowSecrets(rc.caller.runContext)
|
||||
return nil
|
||||
}
|
||||
secrets := callerJob.Secrets()
|
||||
if secrets == nil && callerJob.InheritSecrets() {
|
||||
secrets = rc.caller.runContext.Config.Secrets
|
||||
if err := config.ValidateSecrets(secrets); err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
rc.workflowCallSecrets = make(map[string]string, len(secrets))
|
||||
for k, v := range secrets {
|
||||
var err error
|
||||
if rc.workflowCallSecrets[k], err = callerEval.Interpolate(ctx, v); err != nil {
|
||||
return fmt.Errorf("unable to interpolate secret %s: %w", k, err)
|
||||
}
|
||||
if rc.workflowCallSecrets, err = callerEval.EvaluateCallerSecrets(secrets); err != nil {
|
||||
return fmt.Errorf("unable to interpolate %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -294,6 +294,8 @@ on:
|
||||
name:
|
||||
type: string
|
||||
default: gitea
|
||||
unset:
|
||||
type: boolean
|
||||
`,
|
||||
"workflow_dispatch": `
|
||||
on:
|
||||
@@ -305,6 +307,8 @@ on:
|
||||
name:
|
||||
type: string
|
||||
default: gitea
|
||||
unset:
|
||||
type: boolean
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -356,6 +360,7 @@ on:
|
||||
inputs := getEvaluatorInputs(rc, nil, ghc)
|
||||
assert.Equal(t, table.flag, inputs["flag"])
|
||||
assert.Equal(t, "gitea", inputs["name"])
|
||||
assert.Equal(t, false, inputs["unset"])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -375,11 +380,11 @@ jobs:
|
||||
for _, value := range []string{"true", "false"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
parent, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "call"}, map[string]any{"args": `{"flag":` + value + `,"name":"runner"}`})
|
||||
parent, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "call"}, map[string]any{"args": `{"flag":` + value + `,"name":"${{ 'runner' }}"}`})
|
||||
require.NoError(t, err)
|
||||
child, err := (&runnerImpl{config: &Config{}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]any{"flag": value == "true", "name": "runner"}, child.workflowCallInputs)
|
||||
assert.Equal(t, map[string]any{"flag": value == "true", "name": "${{ 'runner' }}", "unset": false}, child.workflowCallInputs)
|
||||
assert.Equal(t, rawWith, job.RawWith)
|
||||
assert.Nil(t, job.With)
|
||||
})
|
||||
@@ -387,6 +392,46 @@ jobs:
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveWorkflowCallToleratesMismatchesAndPassesInheritedSecretsVerbatim(t *testing.T) {
|
||||
callerWorkflow, err := model.ReadWorkflow(strings.NewReader(`
|
||||
jobs:
|
||||
call:
|
||||
uses: ./reuse.yml
|
||||
with: {flag: not-a-boolean}
|
||||
secrets: {undeclared: "${{ secrets.token }}"}
|
||||
inherit:
|
||||
uses: ./reuse.yml
|
||||
secrets: inherit
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
callee, err := model.ReadWorkflow(strings.NewReader(`
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
flag: {type: boolean, default: true}
|
||||
jobs:
|
||||
test: {}
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
notCallable, err := model.ReadWorkflow(strings.NewReader("on: push\njobs:\n test: {}\n"))
|
||||
require.NoError(t, err)
|
||||
config := &Config{Secrets: map[string]string{"token": "${{ github.token }}"}}
|
||||
resolve := func(callerJobID string, workflow *model.Workflow) *RunContext {
|
||||
parent, err := (&runnerImpl{config: config}).newRunContext(t.Context(), &model.Run{Workflow: callerWorkflow, JobID: callerJobID}, nil)
|
||||
require.NoError(t, err)
|
||||
child, err := (&runnerImpl{config: config, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||
require.NoError(t, err)
|
||||
return child
|
||||
}
|
||||
|
||||
called := resolve("call", callee)
|
||||
assert.Equal(t, map[string]any{"flag": true}, called.workflowCallInputs)
|
||||
assert.Equal(t, map[string]string{"undeclared": "${{ github.token }}"}, called.workflowCallSecrets)
|
||||
inherited := resolve("inherit", notCallable)
|
||||
assert.Empty(t, inherited.workflowCallInputs)
|
||||
assert.Equal(t, config.Secrets, inherited.workflowCallSecrets)
|
||||
}
|
||||
|
||||
func TestJobNameMasksSecrets(t *testing.T) {
|
||||
workflow, err := model.ReadWorkflow(strings.NewReader(`
|
||||
jobs:
|
||||
|
||||
+20
-25
@@ -18,7 +18,6 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
@@ -414,49 +413,45 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
||||
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
||||
}
|
||||
|
||||
// evaluateJobEnvAndDefaults resolves the job's env and defaults.run once, as GitHub does at job setup.
|
||||
// evaluateJobEnvAndDefaults resolves the job's env, defaults.run and container env once, as GitHub does at job setup.
|
||||
func evaluateJobEnvAndDefaults(ctx context.Context, rc *RunContext) error {
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
env := rc.GetEnv()
|
||||
var workflowEnv map[string]string
|
||||
if err := decodeDeferred(ctx, rc.ExprEval, "workflow env", rc.Run.Workflow.RawEnv, &workflowEnv); err != nil {
|
||||
return err
|
||||
if rc.Run.Workflow.Env == nil {
|
||||
if err := model.DecodeEvaluated("workflow env", rc.Run.Workflow.RawEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &workflowEnv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if workflowEnv != nil {
|
||||
rc.Env = mergeMaps(workflowEnv, rc.GetEnv())
|
||||
rc.Env = mergeMaps(workflowEnv, env)
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
}
|
||||
var err error
|
||||
for k, v := range rc.GetEnv() {
|
||||
for k, v := range env {
|
||||
if rc.Env[k], err = rc.ExprEval.Interpolate(ctx, v); err != nil {
|
||||
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
||||
}
|
||||
}
|
||||
defaults := rc.Run.Job().Defaults.Run
|
||||
if rc.jobRunDefaults.Shell, err = rc.ExprEval.Interpolate(ctx, defaults.Shell); err != nil {
|
||||
return fmt.Errorf("unable to interpolate defaults.run.shell: %w", err)
|
||||
var defaults model.Defaults
|
||||
if err := model.DecodeEvaluated("defaults", rc.Run.Job().RawDefaults, rc.ExprEval.shared(ctx).EvaluateYamlNode, &defaults); err != nil {
|
||||
return err
|
||||
}
|
||||
if rc.jobRunDefaults.WorkingDirectory, err = rc.ExprEval.Interpolate(ctx, defaults.WorkingDirectory); err != nil {
|
||||
return fmt.Errorf("unable to interpolate defaults.run.working-directory: %w", err)
|
||||
rc.jobRunDefaults = defaults.Run
|
||||
if rc.containerSpec.Image == "" {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
_, containerEnv := splitContainerEnv(rc.Run.Job().RawContainer)
|
||||
return model.DecodeEvaluated("container env", containerEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &rc.containerSpec.Env)
|
||||
}
|
||||
|
||||
func setJobOutputs(ctx context.Context, rc *RunContext) error {
|
||||
if rc.caller == nil {
|
||||
return nil
|
||||
}
|
||||
outputs := rc.Run.Workflow.WorkflowCallConfig().Outputs
|
||||
callerOutputs := make(map[string]string, len(outputs))
|
||||
ee := sync.OnceValue(func() *expressionEvaluator { return rc.NewExpressionEvaluator(ctx) })
|
||||
for k, v := range outputs {
|
||||
value := v.Value
|
||||
for range 2 { // two passes, the value resolves through a job output
|
||||
var err error
|
||||
if value, err = ee().Interpolate(ctx, value); err != nil {
|
||||
return fmt.Errorf("unable to interpolate workflow output %s: %w", k, err)
|
||||
}
|
||||
}
|
||||
callerOutputs[k] = value
|
||||
callerOutputs, err := rc.NewExpressionEvaluator(ctx).shared(ctx).EvaluateWorkflowCallOutputs(rc.Run.Workflow.WorkflowCallConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate workflow %w", err)
|
||||
}
|
||||
|
||||
// Matrix combinations of a reusable-workflow caller share the caller's *model.Job;
|
||||
@@ -474,7 +469,7 @@ func applyJobTimeout(ctx context.Context, rc *RunContext, job *model.Job) (conte
|
||||
if err != nil {
|
||||
common.Logger(ctx).Errorf("An error occurred when attempting to determine the job timeout: %s", err)
|
||||
} else if timeout != "" {
|
||||
if timeoutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil {
|
||||
if timeoutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil && timeoutMinutes > 0 {
|
||||
return context.WithTimeout(ctx, time.Duration(timeoutMinutes)*time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,8 +479,7 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
Config: &Config{},
|
||||
}
|
||||
if tt.output != "" {
|
||||
rc.Run.Job().Outputs = map[string]string{"bad": tt.output}
|
||||
rc.outputTemplate = map[string]string{"bad": tt.output}
|
||||
require.NoError(t, rc.Run.Job().RawOutputs.Encode(map[string]string{"bad": tt.output}))
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
executorOrder := make([]string, 0)
|
||||
@@ -1082,6 +1081,7 @@ func TestApplyJobTimeout(t *testing.T) {
|
||||
}{
|
||||
{"empty", "runs-on: ubuntu-latest", false},
|
||||
{"integer", "timeout-minutes: 5\nruns-on: ubuntu-latest", true},
|
||||
{"zero ignored", "timeout-minutes: 0\nruns-on: ubuntu-latest", false},
|
||||
{"non-numeric ignored", "timeout-minutes: abc\nruns-on: ubuntu-latest", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
@@ -51,11 +50,7 @@ func (rc *RunContext) runJobHook(ctx context.Context, hookPath, name string) err
|
||||
rawLogger.Infof("shell: %s", shell)
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
if jobContainer := rc.Run.Job().Container(); jobContainer != nil {
|
||||
maps.Copy(env, jobContainer.Env)
|
||||
}
|
||||
maps.Copy(env, rc.GetEnv())
|
||||
env := mergeMaps(rc.containerSpec.Env, rc.GetEnv())
|
||||
rc.withGithubEnv(ctx, rc.getGithubContext(ctx), env)
|
||||
rc.ApplyExtraPath(ctx, &env)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -20,28 +19,22 @@ import (
|
||||
)
|
||||
|
||||
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
if !rc.Config.NoSkipCheckout {
|
||||
fullPath := rc.Run.Job().Uses
|
||||
|
||||
fileName := path.Base(fullPath)
|
||||
workflowDir := strings.TrimSuffix(fullPath, path.Join("/", fileName))
|
||||
workflowDir = strings.TrimPrefix(workflowDir, "./")
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
// resolve the local workflow against the workspace root, not the process
|
||||
// working directory, so it is found regardless of where the runner is invoked
|
||||
newReusableWorkflowExecutor(rc, filepath.Join(rc.Config.Workdir, workflowDir), fileName),
|
||||
)
|
||||
localWorkflow, err := model.ParseReusableWorkflowUses(rc.Run.Job().Uses)
|
||||
if err != nil {
|
||||
return common.NewErrorExecutor(err)
|
||||
}
|
||||
|
||||
// ./.gitea/workflows/wf.yml -> .gitea/workflows/wf.yml
|
||||
trimmedUses := strings.TrimPrefix(rc.Run.Job().Uses, "./")
|
||||
// uses string format is {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}
|
||||
uses := fmt.Sprintf("%s/%s@%s", rc.Config.PresetGitHubContext.Repository, trimmedUses, rc.Config.PresetGitHubContext.Sha)
|
||||
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)
|
||||
}
|
||||
|
||||
remoteReusableWorkflow := newRemoteReusableWorkflowWithPlat(rc.Config.GitHubInstance, uses)
|
||||
if remoteReusableWorkflow == nil {
|
||||
return common.NewErrorExecutor(fmt.Errorf("expected format {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
|
||||
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))
|
||||
@@ -51,37 +44,27 @@ func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.FilePath()),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path),
|
||||
)
|
||||
}
|
||||
|
||||
func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
uses := rc.Run.Job().Uses
|
||||
|
||||
var remoteReusableWorkflow *remoteReusableWorkflow
|
||||
if strings.HasPrefix(uses, "http://") || strings.HasPrefix(uses, "https://") {
|
||||
remoteReusableWorkflow = newRemoteReusableWorkflowFromAbsoluteURL(uses)
|
||||
if remoteReusableWorkflow == nil {
|
||||
return common.NewErrorExecutor(fmt.Errorf("expected format http(s)://{domain}/{owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
|
||||
}
|
||||
} else {
|
||||
remoteReusableWorkflow = newRemoteReusableWorkflowWithPlat(rc.Config.GitHubInstance, uses)
|
||||
if remoteReusableWorkflow == nil {
|
||||
return common.NewErrorExecutor(fmt.Errorf("expected format {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
|
||||
}
|
||||
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.Org, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref)
|
||||
filename := fmt.Sprintf("%s/%s@%s", remoteReusableWorkflow.Owner, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref)
|
||||
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename))
|
||||
|
||||
token := getGitCloneToken(rc.Config, remoteReusableWorkflow.CloneURL())
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.FilePath()),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,7 +73,7 @@ func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
//
|
||||
// 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 newRemoteReusableWorkflowWithPlat
|
||||
// 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, token string) common.Executor {
|
||||
@@ -153,64 +136,34 @@ func newReusableWorkflowRunner(rc *RunContext) (*runnerImpl, error) {
|
||||
}
|
||||
|
||||
type remoteReusableWorkflow struct {
|
||||
URL string
|
||||
Org string
|
||||
Repo string
|
||||
Filename string
|
||||
Ref string
|
||||
|
||||
GitPlatform string
|
||||
*model.ReusableWorkflowUses
|
||||
URL string
|
||||
}
|
||||
|
||||
func (r *remoteReusableWorkflow) CloneURL() string {
|
||||
// In Gitea, r.URL always has the protocol prefix, we don't need to add extra prefix in this case.
|
||||
if strings.HasPrefix(r.URL, "http://") || strings.HasPrefix(r.URL, "https://") {
|
||||
return fmt.Sprintf("%s/%s/%s", r.URL, r.Org, r.Repo)
|
||||
return fmt.Sprintf("%s/%s/%s", r.URL, r.Owner, r.Repo)
|
||||
}
|
||||
return fmt.Sprintf("https://%s/%s/%s", r.URL, r.Org, r.Repo)
|
||||
return fmt.Sprintf("https://%s/%s/%s", r.URL, r.Owner, r.Repo)
|
||||
}
|
||||
|
||||
func (r *remoteReusableWorkflow) FilePath() string {
|
||||
return fmt.Sprintf("./.%s/workflows/%s", r.GitPlatform, r.Filename)
|
||||
}
|
||||
var absoluteReusableWorkflowURLRegex = regexp.MustCompile(`^(https?://.*)/([^/]+/[^/]+/\.[^/]+/workflows/[^@]+@.*)$`)
|
||||
|
||||
// For Gitea
|
||||
// newRemoteReusableWorkflowWithPlat create a `remoteReusableWorkflow`
|
||||
// workflows from `.gitea/workflows` and `.github/workflows` are supported
|
||||
func newRemoteReusableWorkflowWithPlat(url, uses string) *remoteReusableWorkflow {
|
||||
// GitHub docs:
|
||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
|
||||
r := regexp.MustCompile(`^([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`)
|
||||
matches := r.FindStringSubmatch(uses)
|
||||
if len(matches) != 6 {
|
||||
return nil
|
||||
// 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]
|
||||
}
|
||||
return &remoteReusableWorkflow{
|
||||
Org: matches[1],
|
||||
Repo: matches[2],
|
||||
GitPlatform: matches[3],
|
||||
Filename: matches[4],
|
||||
Ref: matches[5],
|
||||
URL: url,
|
||||
parsed, err := model.ParseReusableWorkflowUses(uses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// For Gitea
|
||||
// newRemoteReusableWorkflowWithPlat create a `remoteReusableWorkflow` from an absolute url
|
||||
func newRemoteReusableWorkflowFromAbsoluteURL(uses string) *remoteReusableWorkflow {
|
||||
r := regexp.MustCompile(`^(https?://.*)/([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`)
|
||||
matches := r.FindStringSubmatch(uses)
|
||||
if len(matches) != 7 {
|
||||
return nil
|
||||
}
|
||||
return &remoteReusableWorkflow{
|
||||
URL: matches[1],
|
||||
Org: matches[2],
|
||||
Repo: matches[3],
|
||||
GitPlatform: matches[4],
|
||||
Filename: matches[5],
|
||||
Ref: matches[6],
|
||||
if parsed.IsLocal() {
|
||||
return nil, fmt.Errorf("expected a remote reusable workflow, got %q", uses)
|
||||
}
|
||||
return &remoteReusableWorkflow{ReusableWorkflowUses: parsed, URL: baseURL}, nil
|
||||
}
|
||||
|
||||
// For Gitea
|
||||
|
||||
@@ -5,6 +5,7 @@ package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -106,6 +107,22 @@ func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLocalReusableWorkflowExecutorFindsSameRepositoryPaths(t *testing.T) {
|
||||
workdir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(workdir, ".gitea", "workflows"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workdir, ".gitea", "workflows", "reusable.yml"), []byte(":"), 0o644))
|
||||
|
||||
for _, uses := range []string{"./.gitea/workflows/reusable.yml", "$/.gitea/workflows/reusable.yml"} {
|
||||
rc := &RunContext{
|
||||
Config: &Config{Workdir: workdir},
|
||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {Uses: uses}}}},
|
||||
}
|
||||
err := newLocalReusableWorkflowExecutor(rc)(t.Context())
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, fs.ErrNotExist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) {
|
||||
conf := &Config{
|
||||
GitHubInstance: "gitea.example.net",
|
||||
|
||||
+75
-197
@@ -35,8 +35,10 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/opencontainers/selinux/go-selinux"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
@@ -62,6 +64,7 @@ type RunContext struct {
|
||||
ExprEval *expressionEvaluator
|
||||
JobContainer container.ExecutionsEnvironment
|
||||
serviceContainers []*serviceContainer
|
||||
containerSpec model.ContainerSpec // container:, resolved once with platformImage, zero without a job container
|
||||
JobName string
|
||||
ActionPath string
|
||||
Parent *RunContext
|
||||
@@ -74,6 +77,7 @@ type RunContext struct {
|
||||
workflowCallSecrets map[string]string // the caller's secrets:, resolved once by resolveWorkflowCall
|
||||
jobRunDefaults model.RunDefaults // defaults.run, resolved once at job setup as GitHub does
|
||||
platformImage string // container.image or the runs-on pick, resolved once by isEnabled
|
||||
jobIndex, jobTotal int // strategy.job-index and job-total, total 0 when unknown
|
||||
// summaryFileInitialized tracks which per-step summary files (workflow/step-summary-N.md)
|
||||
// have already been created on the JobContainer. The runner sets up file-command files
|
||||
// via JobContainer.Copy at the start of every phase, which truncates them — fine for
|
||||
@@ -82,10 +86,6 @@ type RunContext struct {
|
||||
// so writes from later phases and from composite sub-steps append to the same file.
|
||||
// Only populated on the top-level RunContext; child RCs walk Parent via topLevelRunContext.
|
||||
summaryFileInitialized map[int]bool
|
||||
// outputTemplate is this combination's pristine snapshot of the job's output expressions,
|
||||
// captured before execution so each matrix combo interpolates from the originals rather
|
||||
// than from a sibling's already-resolved values written into the shared Job.Outputs.
|
||||
outputTemplate map[string]string
|
||||
// jobCancelled records that this job's run was cancelled (context.Canceled). It makes
|
||||
// getJobContext report the "cancelled" status so cancelled()/always() evaluate the way
|
||||
// GitHub Actions does, letting cleanup and always() steps run while normal steps skip.
|
||||
@@ -334,26 +334,12 @@ func splitVolumes(specs []string) ([]string, map[string]string, map[string]bool)
|
||||
}
|
||||
|
||||
// Returns the binds and mounts for the container, resolving paths as appopriate
|
||||
func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string, error) {
|
||||
func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
|
||||
name := rc.jobContainerName()
|
||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||
|
||||
var volumes []string
|
||||
if job := rc.Run.Job(); job != nil {
|
||||
if container := job.Container(); container != nil {
|
||||
for _, v := range container.Volumes {
|
||||
if rc.ExprEval != nil {
|
||||
var err error
|
||||
if v, err = rc.ExprEval.Interpolate(context.Background(), v); err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to interpolate container.volumes: %w", err)
|
||||
}
|
||||
}
|
||||
volumes = append(volumes, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
// the runner's own mounts below yield to the targets the job claims
|
||||
binds, mounts, claimed := splitVolumes(volumes)
|
||||
binds, mounts, claimed := splitVolumes(rc.containerSpec.Volumes)
|
||||
|
||||
if rc.containerDaemonSocket() != "-" && !claimed["/var/run/docker.sock"] {
|
||||
binds = append(binds, rc.jobDockerSocket()+":/var/run/docker.sock")
|
||||
@@ -385,7 +371,7 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return binds, mounts, nil
|
||||
return binds, mounts
|
||||
}
|
||||
|
||||
func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
@@ -486,7 +472,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
image := rc.platformImage
|
||||
logWriter := rc.commandLogWriter(ctx)
|
||||
|
||||
username, password, err := rc.handleCredentials(ctx)
|
||||
username, password, err := registryCredentials(rc.containerSpec.Credentials, "container.")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to handle credentials: %s", err)
|
||||
}
|
||||
@@ -507,78 +493,57 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
networkName, createAndDeleteNetwork := rc.networkNameForGitea()
|
||||
rc.cleanUpJobContainer = rc.cleanupJobResources(networkName, createAndDeleteNetwork, false)
|
||||
|
||||
services := rc.Run.Job().Services
|
||||
if err := decodeDeferred(ctx, rc.ExprEval, "job services", rc.Run.Job().RawServices, &services); err != nil {
|
||||
var services map[string]*model.ContainerSpec
|
||||
if err := model.DecodeEvaluated("job services", rc.Run.Job().RawServices, rc.ExprEval.shared(ctx).EvaluateYamlNode, &services); err != nil {
|
||||
return err
|
||||
}
|
||||
for serviceID, spec := range services {
|
||||
if spec == nil {
|
||||
return fmt.Errorf("service %s has no container definition", serviceID)
|
||||
}
|
||||
// GitHub compatibility: skip services whose image evaluates to an
|
||||
// empty string, enabling conditional services via expressions
|
||||
serviceImage, err := rc.ExprEval.Interpolate(ctx, spec.Image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate service %s image: %w", serviceID, err)
|
||||
}
|
||||
if serviceImage == "" {
|
||||
if spec.Image == "" {
|
||||
logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
|
||||
continue
|
||||
}
|
||||
// interpolate env
|
||||
interpolatedEnvs := make(map[string]string, len(spec.Env)+len(rc.Config.ProxyEnv))
|
||||
envs := make([]string, 0, len(spec.Env)+len(rc.Config.ProxyEnv))
|
||||
// a service reaches the internet the way the job does; its own env still wins
|
||||
maps0.Copy(interpolatedEnvs, rc.Config.ProxyEnv)
|
||||
for k, v := range spec.Env {
|
||||
if interpolatedEnvs[k], err = rc.ExprEval.Interpolate(ctx, v); err != nil {
|
||||
return fmt.Errorf("unable to interpolate service %s env %s: %w", serviceID, k, err)
|
||||
}
|
||||
}
|
||||
envs := make([]string, 0, len(interpolatedEnvs))
|
||||
for k, v := range interpolatedEnvs {
|
||||
for k, v := range mergeMaps(rc.Config.ProxyEnv, spec.Env) {
|
||||
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
// interpolate cmd
|
||||
interpolatedCmd := make([]string, len(spec.Cmd))
|
||||
for i, v := range spec.Cmd {
|
||||
if interpolatedCmd[i], err = rc.ExprEval.Interpolate(ctx, v); err != nil {
|
||||
return fmt.Errorf("unable to interpolate service %s command: %w", serviceID, err)
|
||||
}
|
||||
}
|
||||
// keep these local: reusing username/password would overwrite the
|
||||
// credentials the job container is pulled with further down
|
||||
serviceUsername, servicePassword, err := rc.interpolateCredentials(ctx, spec.Credentials, "")
|
||||
serviceUsername, servicePassword, err := registryCredentials(spec.Credentials, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
|
||||
}
|
||||
|
||||
interpolatedVolumes := make([]string, len(spec.Volumes))
|
||||
for i, volume := range spec.Volumes {
|
||||
if interpolatedVolumes[i], err = rc.ExprEval.Interpolate(ctx, volume); err != nil {
|
||||
return fmt.Errorf("unable to interpolate service %s volumes: %w", serviceID, err)
|
||||
cmd := spec.Cmd
|
||||
if len(cmd) == 0 && spec.Command != "" {
|
||||
if cmd, err = shellquote.Split(spec.Command); err != nil {
|
||||
return fmt.Errorf("failed to parse service %s command: %w", serviceID, err)
|
||||
}
|
||||
}
|
||||
serviceBinds, serviceMounts, _ := splitVolumes(interpolatedVolumes)
|
||||
|
||||
interpolatedPorts := make([]string, len(spec.Ports))
|
||||
for i, port := range spec.Ports {
|
||||
if interpolatedPorts[i], err = rc.ExprEval.Interpolate(ctx, port); err != nil {
|
||||
return fmt.Errorf("unable to interpolate service %s ports: %w", serviceID, err)
|
||||
}
|
||||
var entrypoint []string
|
||||
if spec.Entrypoint != "" {
|
||||
entrypoint = []string{spec.Entrypoint}
|
||||
}
|
||||
exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts)
|
||||
|
||||
serviceBinds, serviceMounts, _ := splitVolumes(spec.Volumes)
|
||||
|
||||
exposedPorts, portBindings, err := nat.ParsePortSpecs(spec.Ports)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse service %s ports: %w", serviceID, err)
|
||||
}
|
||||
serviceOptions, err := rc.ExprEval.Interpolate(ctx, spec.Options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate service %s options: %w", serviceID, err)
|
||||
}
|
||||
|
||||
serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
|
||||
c := newContainer(&container.NewContainerInput{
|
||||
Name: serviceContainerName,
|
||||
Image: serviceImage,
|
||||
Image: spec.Image,
|
||||
Username: serviceUsername,
|
||||
Password: servicePassword,
|
||||
Cmd: interpolatedCmd,
|
||||
Entrypoint: entrypoint,
|
||||
Cmd: cmd,
|
||||
Env: envs,
|
||||
Mounts: serviceMounts,
|
||||
Binds: serviceBinds,
|
||||
@@ -588,7 +553,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
UsernsMode: rc.Config.UsernsMode,
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
AutoRemove: false, // so a dead service's log survives, cleanupJobResources removes it
|
||||
WorkflowOptions: serviceOptions,
|
||||
WorkflowOptions: spec.Options,
|
||||
NetworkMode: networkName,
|
||||
NetworkAliases: []string{serviceID},
|
||||
ExposedPorts: exposedPorts,
|
||||
@@ -596,16 +561,12 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
ValidVolumes: rc.Config.ValidVolumes, // not validVolumes(), a service gets no docker socket
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
})
|
||||
rc.serviceContainers = append(rc.serviceContainers, &serviceContainer{name: serviceID, image: serviceImage, container: c})
|
||||
rc.serviceContainers = append(rc.serviceContainers, &serviceContainer{name: serviceID, image: spec.Image, container: c})
|
||||
}
|
||||
|
||||
// For Gitea, `jobContainerNetwork` should be the same as `networkName`
|
||||
jobContainerNetwork := networkName
|
||||
|
||||
workflowOptions, err := rc.workflowOptions(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||
containerInput := &container.NewContainerInput{
|
||||
Cmd: nil,
|
||||
@@ -624,7 +585,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
UsernsMode: rc.Config.UsernsMode,
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
RunnerOptions: rc.Config.ContainerOptions,
|
||||
WorkflowOptions: workflowOptions,
|
||||
WorkflowOptions: rc.containerSpec.Options,
|
||||
AutoRemove: true,
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
}
|
||||
@@ -641,9 +602,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
return err
|
||||
}
|
||||
rc.startDockerProxy(ctx)
|
||||
if containerInput.Binds, containerInput.Mounts, err = rc.GetBindsAndMounts(); err != nil {
|
||||
return err
|
||||
}
|
||||
containerInput.Binds, containerInput.Mounts = rc.GetBindsAndMounts()
|
||||
containerInput.ValidVolumes = rc.validVolumes()
|
||||
|
||||
rc.jobNetworkName = networkName
|
||||
@@ -1033,39 +992,16 @@ func lockJob(job *model.Job) func() {
|
||||
|
||||
func (rc *RunContext) interpolateOutputs() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
ee := rc.NewExpressionEvaluator(ctx)
|
||||
job := rc.Run.Job()
|
||||
// Matrix combinations share this Job and its Outputs map. Interpolate from this combo's
|
||||
// pristine snapshot (outputTemplate) and write under the lock, so each combo overwrites
|
||||
// with its own resolved values (last wins, as on GitHub) instead of the first combo's
|
||||
// resolved values freezing the shared template against later combos.
|
||||
// Resolved up front so one failure publishes none of them, as GitHub does.
|
||||
var deferredOutputs map[string]string
|
||||
if err := decodeDeferred(ctx, ee, "job outputs", job.RawOutputs, &deferredOutputs); err != nil {
|
||||
var outputs map[string]string
|
||||
if err := model.DecodeEvaluated("job outputs", job.RawOutputs, rc.NewExpressionEvaluator(ctx).shared(ctx).EvaluateYamlNode, &outputs); err != nil {
|
||||
return err
|
||||
}
|
||||
if deferredOutputs != nil {
|
||||
defer lockJob(job)()
|
||||
job.Outputs = deferredOutputs
|
||||
return nil
|
||||
}
|
||||
outputs := make(map[string]string, len(rc.outputTemplate))
|
||||
var err error
|
||||
for k, v := range rc.outputTemplate {
|
||||
if outputs[k], err = ee.Interpolate(ctx, v); err != nil {
|
||||
err = fmt.Errorf("failed to evaluate job output %q: %w", k, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Matrix combinations share the job, so as on GitHub a failure publishes nothing and the last non-empty value wins.
|
||||
maps0.DeleteFunc(outputs, func(_, value string) bool { return value == "" })
|
||||
defer lockJob(job)()
|
||||
for k := range rc.outputTemplate {
|
||||
if err != nil {
|
||||
job.Outputs[k] = ""
|
||||
continue
|
||||
}
|
||||
job.Outputs[k] = outputs[k]
|
||||
}
|
||||
return err
|
||||
job.Outputs = mergeMaps(job.Outputs, outputs)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1184,18 +1120,6 @@ func (rc *RunContext) Executor() (common.Executor, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rc *RunContext) containerImage(ctx context.Context) (string, error) {
|
||||
c := rc.Run.Job().Container()
|
||||
if c == nil {
|
||||
return "", nil
|
||||
}
|
||||
image, err := rc.ExprEval.Interpolate(ctx, c.Image)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to interpolate container.image: %w", err)
|
||||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (rc *RunContext) runsOnImage(ctx context.Context) (string, error) {
|
||||
if rc.Run.Job().RunsOn() == nil {
|
||||
common.Logger(ctx).Errorf("'runs-on' key not defined in %s", rc.String())
|
||||
@@ -1233,26 +1157,37 @@ func (rc *RunContext) runsOnPlatformNames(ctx context.Context) []string {
|
||||
return model.RunsOnFromNode(rawRunsOn)
|
||||
}
|
||||
|
||||
// resolvePlatformImage evaluates the job's image once, so every consumer sees the image the job started with.
|
||||
// resolvePlatformImage evaluates the job's container once for every consumer, ignoring one without an image as GitHub does.
|
||||
func (rc *RunContext) resolvePlatformImage(ctx context.Context) error {
|
||||
image, err := rc.containerImage(ctx)
|
||||
if err == nil && image == "" {
|
||||
image, err = rc.runsOnImage(ctx)
|
||||
withoutEnv, _ := splitContainerEnv(rc.Run.Job().RawContainer)
|
||||
var spec *model.ContainerSpec
|
||||
if err := model.DecodeEvaluated("container", withoutEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &spec); err != nil {
|
||||
return err
|
||||
}
|
||||
if spec != nil && spec.Image != "" {
|
||||
rc.containerSpec, rc.platformImage = *spec, spec.Image
|
||||
return nil
|
||||
}
|
||||
image, err := rc.runsOnImage(ctx)
|
||||
rc.platformImage = image
|
||||
return err
|
||||
}
|
||||
|
||||
func (rc *RunContext) workflowOptions(ctx context.Context) (string, error) {
|
||||
c := rc.Run.Job().Container()
|
||||
if c == nil {
|
||||
return "", nil
|
||||
// splitContainerEnv takes `env` out of a container mapping, since env reads the job env that resolves after the job environment starts.
|
||||
func splitContainerEnv(container yaml.Node) (withoutEnv, env yaml.Node) {
|
||||
if container.Kind != yaml.MappingNode {
|
||||
return container, env
|
||||
}
|
||||
options, err := rc.ExprEval.Interpolate(ctx, c.Options)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to interpolate container.options: %w", err)
|
||||
withoutEnv = container
|
||||
withoutEnv.Content = nil
|
||||
for i := 0; i+1 < len(container.Content); i += 2 {
|
||||
if container.Content[i].Value == "env" {
|
||||
env = *container.Content[i+1]
|
||||
} else {
|
||||
withoutEnv.Content = append(withoutEnv.Content, container.Content[i:i+2]...)
|
||||
}
|
||||
}
|
||||
return options, nil
|
||||
return withoutEnv, env
|
||||
}
|
||||
|
||||
func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
||||
@@ -1471,22 +1406,11 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
||||
|
||||
{ // Adapt to Gitea
|
||||
if preset := rc.Config.PresetGitHubContext; preset != nil {
|
||||
ghc.Event = preset.Event
|
||||
ghc.RunID = preset.RunID
|
||||
ghc.RunNumber = preset.RunNumber
|
||||
ghc.RunAttempt = preset.RunAttempt
|
||||
ghc.Actor = preset.Actor
|
||||
ghc.Repository = preset.Repository
|
||||
ghc.EventName = preset.EventName
|
||||
ghc.Sha = preset.Sha
|
||||
ghc.Ref = preset.Ref
|
||||
ghc.RefName = preset.RefName
|
||||
ghc.RefType = preset.RefType
|
||||
ghc.HeadRef = preset.HeadRef
|
||||
ghc.BaseRef = preset.BaseRef
|
||||
ghc.Token = preset.Token
|
||||
ghc.RepositoryOwner = preset.RepositoryOwner
|
||||
ghc.RetentionDays = preset.RetentionDays
|
||||
merged := *preset
|
||||
merged.Workflow, merged.Job, merged.Workspace, merged.EventPath = ghc.Workflow, ghc.Job, ghc.Workspace, ghc.EventPath
|
||||
merged.Action, merged.ActionPath, merged.ActionRepository, merged.ActionRef = ghc.Action, ghc.ActionPath, ghc.ActionRepository, ghc.ActionRef
|
||||
merged.RunnerPerflog, merged.RunnerTrackingID = ghc.RunnerPerflog, ghc.RunnerTrackingID
|
||||
*ghc = merged
|
||||
|
||||
instance := rc.Config.GitHubInstance
|
||||
if !strings.HasPrefix(instance, "http://") &&
|
||||
@@ -1563,12 +1487,7 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
|
||||
if step.Type() != model.StepTypeUsesActionRemote {
|
||||
return false
|
||||
}
|
||||
remoteAction := newRemoteAction(step.Uses)
|
||||
if remoteAction == nil {
|
||||
// IsCheckout() will nil panic if we dont bail out early
|
||||
return false
|
||||
}
|
||||
if !remoteAction.IsCheckout() {
|
||||
if uses, err := model.ParseActionUses(step.Uses); err != nil || uses.Owner != "actions" || uses.Repo != "checkout" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1581,23 +1500,6 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func nestedMapLookup(m map[string]any, ks ...string) (rval any) {
|
||||
var ok bool
|
||||
|
||||
if len(ks) == 0 { // degenerate input
|
||||
return nil
|
||||
}
|
||||
if rval, ok = m[ks[0]]; !ok {
|
||||
return nil
|
||||
} else if len(ks) == 1 { // we've reached the final key
|
||||
return rval
|
||||
} else if m, ok = rval.(map[string]any); !ok {
|
||||
return nil
|
||||
} else { // 1+ more keys
|
||||
return nestedMapLookup(m, ks[1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) {
|
||||
env["CI"] = "true"
|
||||
env["GITHUB_WORKFLOW"] = github.Workflow
|
||||
@@ -1721,41 +1623,17 @@ func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
||||
env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken
|
||||
}
|
||||
|
||||
func (rc *RunContext) handleCredentials(ctx context.Context) (string, string, error) {
|
||||
container := rc.Run.Job().Container()
|
||||
if container == nil || container.Credentials == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
return rc.interpolateCredentials(ctx, container.Credentials, "container.")
|
||||
}
|
||||
|
||||
func (rc *RunContext) interpolateCredentials(ctx context.Context, credentials map[string]string, prefix string) (string, string, error) {
|
||||
func registryCredentials(credentials map[string]string, prefix string) (string, string, error) {
|
||||
if credentials == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
if len(credentials) != 2 {
|
||||
return "", "", errors.New("invalid property count for key 'credentials:'")
|
||||
}
|
||||
|
||||
ee := rc.NewExpressionEvaluator(ctx)
|
||||
interpolate := func(key string) (string, error) {
|
||||
value, err := ee.Interpolate(ctx, credentials[key])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to interpolate %scredentials.%s: %w", prefix, key, err)
|
||||
for _, key := range []string{"username", "password"} {
|
||||
if credentials[key] == "" {
|
||||
return "", "", fmt.Errorf("failed to interpolate %scredentials.%s", prefix, key)
|
||||
}
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("failed to interpolate %scredentials.%s", prefix, key)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
username, err := interpolate("username")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
password, err := interpolate("password")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return username, password, nil
|
||||
return credentials["username"], credentials["password"], nil
|
||||
}
|
||||
@@ -185,38 +185,6 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunContextHandleCredentialsDoesNotUseDockerSecrets(t *testing.T) {
|
||||
workflow, err := model.ReadWorkflow(strings.NewReader(`
|
||||
name: test
|
||||
on: push
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps: []
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
|
||||
rc := &RunContext{
|
||||
Config: &Config{
|
||||
Secrets: map[string]string{
|
||||
"DOCKER_USERNAME": "docker-user",
|
||||
"DOCKER_PASSWORD": "docker-password",
|
||||
},
|
||||
Env: map[string]string{},
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "job",
|
||||
Workflow: workflow,
|
||||
},
|
||||
}
|
||||
|
||||
// DOCKER_USERNAME/DOCKER_PASSWORD secrets should not be used as implicit job container pull credentials.
|
||||
username, password, err := rc.handleCredentials(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, username)
|
||||
assert.Empty(t, password)
|
||||
}
|
||||
|
||||
// fakeContainer turns every container operation into a no-op, so startJobContainer
|
||||
// runs without a Docker daemon. The embedded interface is nil, so any method the
|
||||
// test does not exercise panics rather than silently doing the wrong thing.
|
||||
@@ -370,19 +338,28 @@ jobs:
|
||||
require.Equal(t, map[string]string{"data": "/data"}, redis.Mounts)
|
||||
require.Empty(t, redis.Binds) // the docker socket is the job container's alone
|
||||
require.Empty(t, redis.WorkingDir)
|
||||
require.Nil(t, redis.Entrypoint)
|
||||
require.Nil(t, redis.Cmd)
|
||||
}
|
||||
|
||||
// A whole-value `services:` expression only reaches the typed field through DecodeRaw.
|
||||
func TestStartJobContainerGivesServicesTheirVolumesFromExpression(t *testing.T) {
|
||||
redis := startJobContainerInputs(t, `
|
||||
func TestStartJobContainerEvaluatesContainersOnce(t *testing.T) {
|
||||
inputs := startJobContainerInputs(t, `
|
||||
jobs:
|
||||
job:
|
||||
services: ${{ fromJSON('{"redis":{"image":"redis:latest","volumes":["data:/data"]}}') }}
|
||||
`, &Config{ValidVolumes: []string{"data"}})[0]
|
||||
container: ${{ fromJSON('{"image":"node:20","options":"--label ${{ github.job }}","credentials":{"username":"user","password":"${{ vars.TITLE }}"}}') }}
|
||||
services: ${{ fromJSON('{"redis":{"image":"redis:latest","volumes":["data:/data"],"env":{"TITLE":"${{ github.job }}"},"credentials":{"username":"user","password":"${{ github.job }}"},"entrypoint":"/entry.sh","command":"redis-server --port 6380"}}') }}
|
||||
`, &Config{})
|
||||
redis, job := inputs[0], inputs[1]
|
||||
|
||||
require.Equal(t, "redis:latest", redis.Image)
|
||||
require.Equal(t, []string{"data"}, redis.ValidVolumes)
|
||||
require.Equal(t, map[string]string{"data": "/data"}, redis.Mounts)
|
||||
require.Equal(t, []string{"TITLE=${{ github.job }}"}, redis.Env)
|
||||
require.Equal(t, "${{ github.job }}", redis.Password)
|
||||
require.Equal(t, []string{"/entry.sh"}, redis.Entrypoint)
|
||||
require.Equal(t, []string{"redis-server", "--port", "6380"}, redis.Cmd)
|
||||
require.Equal(t, "node:20", job.Image)
|
||||
require.Equal(t, "--label ${{ github.job }}", job.WorkflowOptions)
|
||||
require.Equal(t, "${{ vars.TITLE }}", job.Password)
|
||||
}
|
||||
|
||||
// Only the workflow's options may be stripped later, so the two sources have to reach the
|
||||
@@ -506,8 +483,7 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
config := testcase.rc.Config
|
||||
config.Workdir = testcase.name
|
||||
config.BindWorkdir = bindWorkDir
|
||||
gotbind, gotmount, err := rctemplate.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
gotbind, gotmount := rctemplate.GetBindsAndMounts()
|
||||
|
||||
// Name binds/mounts are either/or
|
||||
if config.BindWorkdir {
|
||||
@@ -543,8 +519,9 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
|
||||
t.Run("InterpolatedContainerVolumes", func(t *testing.T) {
|
||||
job := &model.Job{}
|
||||
err := job.RawContainer.Encode(map[string][]string{
|
||||
"volumes": {"${{ secrets.MAME }}:/root/.mame/roms:ro"},
|
||||
err := job.RawContainer.Encode(map[string]any{
|
||||
"image": "node:20",
|
||||
"volumes": []string{"${{ secrets.MAME }}:/root/.mame/roms:ro"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -565,9 +542,9 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
rc.Run.JobID = "job1"
|
||||
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
require.NoError(t, rc.resolvePlatformImage(context.Background()))
|
||||
|
||||
gotbind, gotmount, err := rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
gotbind, gotmount := rc.GetBindsAndMounts()
|
||||
assert.Contains(t, gotbind, "/host/mame/roms:/root/.mame/roms:ro")
|
||||
assert.NotContains(t, gotbind, "${{ secrets.MAME }}")
|
||||
assert.NotContains(t, gotmount, "${{ secrets.MAME }}")
|
||||
@@ -575,12 +552,6 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
|
||||
for _, testcase := range tests {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
job := &model.Job{}
|
||||
err := job.RawContainer.Encode(map[string][]string{
|
||||
"volumes": testcase.volumes,
|
||||
})
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
rc := &RunContext{
|
||||
Name: "TestRCName",
|
||||
Run: &model.Run{
|
||||
@@ -592,12 +563,11 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
BindWorkdir: false,
|
||||
SharedToolCache: true, // so OverridesToolCache has a mount to displace
|
||||
},
|
||||
containerSpec: model.ContainerSpec{Volumes: testcase.volumes},
|
||||
}
|
||||
rc.Run.JobID = "job1"
|
||||
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}
|
||||
|
||||
gotbind, gotmount, err := rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
gotbind, gotmount := rc.GetBindsAndMounts()
|
||||
|
||||
if len(testcase.wantbind) > 0 {
|
||||
assert.Contains(t, gotbind, testcase.wantbind)
|
||||
@@ -632,13 +602,11 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
Config: &Config{},
|
||||
}
|
||||
|
||||
_, gotmount, err := rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
_, gotmount := rc.GetBindsAndMounts()
|
||||
assert.NotContains(t, gotmount, sharedToolCacheVolume)
|
||||
|
||||
rc.Config.SharedToolCache = true
|
||||
_, gotmount, err = rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
_, gotmount = rc.GetBindsAndMounts()
|
||||
assert.Equal(t, container.DefaultToolCache, gotmount[sharedToolCacheVolume])
|
||||
})
|
||||
|
||||
@@ -651,18 +619,15 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
Config: &Config{BindWorkdir: true, Workdir: "/workspace/1/owner/repo", PresetGitHubContext: &model.GithubContext{}},
|
||||
}
|
||||
|
||||
gotbind, _, err := rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
gotbind, _ := rc.GetBindsAndMounts()
|
||||
assert.True(t, slices.ContainsFunc(gotbind, func(bind string) bool { return strings.HasPrefix(bind, "/workspace/1:/workspace/1") }), gotbind)
|
||||
|
||||
rc.Config.BindWorkdir = false
|
||||
_, gotmount, err := rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
_, gotmount := rc.GetBindsAndMounts()
|
||||
assert.Equal(t, "/workspace/1", gotmount[rc.jobContainerName()])
|
||||
|
||||
require.NoError(t, rc.Run.Job().RawContainer.Encode(map[string][]string{"volumes": {"claimed:/workspace/1"}}))
|
||||
_, gotmount, err = rc.GetBindsAndMounts()
|
||||
require.NoError(t, err)
|
||||
rc.containerSpec.Volumes = []string{"claimed:/workspace/1"}
|
||||
_, gotmount = rc.GetBindsAndMounts()
|
||||
assert.Equal(t, "/workspace/1/owner/repo", gotmount[rc.jobContainerName()])
|
||||
})
|
||||
}
|
||||
@@ -845,25 +810,30 @@ func TestCleanupJobResourcesContinuesAfterFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInterpolateOutputsIsPerMatrixCombo guards the matrix-output fix: combinations share one
|
||||
// *model.Job, so each must interpolate from its own pristine snapshot. Otherwise the first
|
||||
// combo's resolved value freezes the shared template and later combos can't resolve their own.
|
||||
func TestInterpolateOutputsIsPerMatrixCombo(t *testing.T) {
|
||||
func TestInterpolateOutputsIsPerMatrixComboKeepingNeedsOutputs(t *testing.T) {
|
||||
job := &model.Job{Outputs: map[string]string{"o": "${{ matrix.v }}"}}
|
||||
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job}}}
|
||||
require.NoError(t, job.RawOutputs.Encode(job.Outputs))
|
||||
needed := &model.Job{Outputs: map[string]string{"o": "from-gitea"}}
|
||||
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job, "needed": needed}}}
|
||||
r := &runnerImpl{config: &Config{}}
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := r.newRunContext(ctx, &model.Run{JobID: "needed", Workflow: run.Workflow}, nil)
|
||||
require.NoError(t, err)
|
||||
rcA, err := r.newRunContext(ctx, run, map[string]any{"v": "a"})
|
||||
require.NoError(t, err)
|
||||
rcB, err := r.newRunContext(ctx, run, map[string]any{"v": "b"})
|
||||
require.NoError(t, err)
|
||||
rcEmpty, err := r.newRunContext(ctx, run, map[string]any{"v": ""})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, job.Outputs)
|
||||
|
||||
require.NoError(t, rcA.interpolateOutputs()(ctx))
|
||||
require.NoError(t, rcB.interpolateOutputs()(ctx))
|
||||
require.NoError(t, rcEmpty.interpolateOutputs()(ctx))
|
||||
|
||||
// Last combo wins (matching GitHub) instead of being frozen to combo A's "a".
|
||||
require.Equal(t, "b", job.Outputs["o"])
|
||||
require.Equal(t, map[string]string{"o": "b"}, job.Outputs)
|
||||
require.Equal(t, map[string]string{"o": "from-gitea"}, needed.Outputs)
|
||||
}
|
||||
|
||||
// A whole-value `outputs:` expression only reaches the typed field through DecodeRaw.
|
||||
@@ -936,6 +906,12 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
assert.Equal(t, ghc.RunnerPerflog, "/dev/null") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, ghc.Token, rc.Config.Secrets["GITHUB_TOKEN"])
|
||||
assert.Equal(t, ghc.Job, "job1") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
rc.Config.PresetGitHubContext = &model.GithubContext{Actor: "preset-actor", TriggeringActor: "triggerer", Job: "preset-job"}
|
||||
ghc = rc.getGithubContext(context.Background())
|
||||
assert.Equal(t, "preset-actor", ghc.Actor)
|
||||
assert.Equal(t, "triggerer", ghc.TriggeringActor)
|
||||
assert.Equal(t, "job1", ghc.Job)
|
||||
}
|
||||
|
||||
func TestGetGithubContextRef(t *testing.T) {
|
||||
|
||||
+26
-9
@@ -7,7 +7,6 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
@@ -159,9 +158,9 @@ func (runner *runnerImpl) configure() (*runnerImpl, error) {
|
||||
func maxParallelFor(strategy *model.Strategy, combinations int) int {
|
||||
maxParallel := 4 // actionslib has no default for an undeclared max-parallel
|
||||
if strategy != nil {
|
||||
if limit, declared, err := strategy.ParseMaxParallel(); err != nil {
|
||||
if limit, err := strategy.MaxParallel(); err != nil {
|
||||
log.Errorf("Ignoring invalid max-parallel: %v", err)
|
||||
} else if declared {
|
||||
} else if limit > 0 {
|
||||
maxParallel = limit
|
||||
}
|
||||
}
|
||||
@@ -210,7 +209,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
return err
|
||||
}
|
||||
if job.RawStrategy.Kind == yaml.ScalarNode {
|
||||
if err := decodeDeferred(ctx, strategyRc.ExprEval, "job strategy", job.RawStrategy, &job.Strategy); err != nil {
|
||||
if err := model.DecodeEvaluated("job strategy", job.RawStrategy, strategyRc.ExprEval.shared(ctx).EvaluateYamlNode, &job.Strategy); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -222,6 +221,11 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil {
|
||||
log.Errorf("Error while evaluating matrix: %v", err)
|
||||
}
|
||||
for _, value := range []*string{&job.Strategy.FailFastString, &job.Strategy.MaxParallelString} {
|
||||
if evaluated, err := strategyRc.ExprEval.Interpolate(ctx, *value); err == nil {
|
||||
*value = evaluated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +240,16 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
log.Infof("Running job with maxParallel=%d for %d matrix combinations", maxParallel, len(matrixes))
|
||||
|
||||
for i, matrix := range matrixes {
|
||||
rc, err := runner.newRunContext(ctx, run, matrix)
|
||||
index, total := i, len(matrixes)
|
||||
if runner.caller == nil && runner.config.PresetGitHubContext != nil && len(matrix) > 0 {
|
||||
var expanded struct {
|
||||
Index int `yaml:"job-index"`
|
||||
Total int `yaml:"job-total"`
|
||||
}
|
||||
_ = job.RawStrategy.Decode(&expanded) // Gitea expanded the combination, a version writing neither leaves them unknown
|
||||
index, total = expanded.Index, expanded.Total
|
||||
}
|
||||
rc, err := runner.newCombinationRunContext(ctx, run, matrix, index, total)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -325,6 +338,10 @@ func handleFailure(plan *model.Plan) common.Executor {
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]any) (*RunContext, error) {
|
||||
return runner.newCombinationRunContext(ctx, run, matrix, 0, 1)
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) newCombinationRunContext(ctx context.Context, run *model.Run, matrix map[string]any, index, total int) (*RunContext, error) {
|
||||
rc := &RunContext{
|
||||
Config: runner.config,
|
||||
Run: run,
|
||||
@@ -332,16 +349,16 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat
|
||||
StepResults: make(map[string]*model.StepResult),
|
||||
Matrix: matrix,
|
||||
caller: runner.caller,
|
||||
jobIndex: index,
|
||||
jobTotal: total,
|
||||
}
|
||||
if err := rc.resolveWorkflowCall(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
rc.Name = rc.maskSecrets(rc.ExprEval.InterpolateName(ctx, run.String()))
|
||||
// Snapshot the job's pristine output expressions now, before any matrix combo runs and
|
||||
// rewrites the shared Job.Outputs (see interpolateOutputs).
|
||||
if job := run.Job(); job != nil {
|
||||
rc.outputTemplate = maps.Clone(job.Outputs)
|
||||
if job := run.Job(); job != nil && job.RawOutputs.Kind != 0 {
|
||||
job.Outputs = nil // a job no combination ran for reads "" as on GitHub, a Gitea needs placeholder has no RawOutputs and keeps its outputs
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
|
||||
@@ -7,6 +7,7 @@ package runner
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -29,6 +30,7 @@ var (
|
||||
logLevel = log.DebugLevel
|
||||
workdir = "testdata"
|
||||
secrets map[string]string
|
||||
testRunID = strings.ToLower(rand.Text()[:8]) // keeps docker names apart from concurrent test processes on one daemon
|
||||
)
|
||||
|
||||
func mapPlatformPicker(platforms map[string]string) func([]string) string {
|
||||
@@ -216,7 +218,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
EventPath: cfg.EventPath,
|
||||
PlatformPicker: mapPlatformPicker(j.platforms),
|
||||
// fixtures reuse workflow and job names, so parallel tests would collide without this
|
||||
ContainerNamePrefix: strings.ReplaceAll(t.Name(), "/", "-"),
|
||||
ContainerNamePrefix: strings.ReplaceAll(t.Name(), "/", "-") + "-" + testRunID,
|
||||
// 0 would run jobs runtime.NumCPU()-wide, making the network peak machine-dependent
|
||||
MaxParallel: 2,
|
||||
ForceRebuild: true,
|
||||
@@ -346,7 +348,7 @@ func TestRunEvent(t *testing.T) {
|
||||
|
||||
config := &Config{
|
||||
Secrets: table.secrets,
|
||||
Env: map[string]string{"GITHUB_REPOSITORY": t.Name()},
|
||||
Env: map[string]string{"GITHUB_REPOSITORY": t.Name() + "-" + testRunID},
|
||||
}
|
||||
|
||||
eventFile := filepath.Join(workdir, table.workflowPath, "event.json")
|
||||
|
||||
+22
-33
@@ -11,7 +11,6 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
|
||||
"gitea.dev/actionslib/pkg/exprparser"
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type step interface {
|
||||
@@ -89,6 +87,9 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
if err == nil {
|
||||
runStep, err = isStepEnabled(ctx, ifExpression, step, stage)
|
||||
}
|
||||
if err == nil && runStep {
|
||||
err = setupInputs(ctx, step)
|
||||
}
|
||||
if err != nil {
|
||||
stepResult.Conclusion = model.StepStatusFailure
|
||||
stepResult.Outcome = model.StepStatusFailure
|
||||
@@ -240,51 +241,39 @@ func setupEnv(ctx context.Context, step step) error {
|
||||
rc := step.getRunContext()
|
||||
|
||||
mergeEnv(ctx, step)
|
||||
// merge step env last, since it should not be overwritten
|
||||
mergeIntoMap(step, step.getEnv(), step.getStepModel().GetEnv())
|
||||
|
||||
var err error
|
||||
exprEval := rc.NewExpressionEvaluator(ctx)
|
||||
for k, v := range *step.getEnv() {
|
||||
if !strings.HasPrefix(k, "INPUT_") {
|
||||
if (*step.getEnv())[k], err = exprEval.Interpolate(ctx, v); err != nil {
|
||||
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
||||
}
|
||||
stepEnv := step.getStepModel().Environment()
|
||||
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)
|
||||
}
|
||||
}
|
||||
// after we have an evaluated step context, update the expressions evaluator with a new env context
|
||||
// you can use step level env in the with property of a uses construct
|
||||
inputEval := sync.OnceValue(func() *expressionEvaluator { return rc.NewExpressionEvaluatorWithEnv(ctx, *step.getEnv()) })
|
||||
for k, v := range *step.getEnv() {
|
||||
if strings.HasPrefix(k, "INPUT_") {
|
||||
if (*step.getEnv())[k], err = inputEval().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 step.getStepModel().RawWith.Kind == yaml.ScalarNode {
|
||||
decoded := &model.Step{}
|
||||
if err := decodeDeferred(ctx, inputEval(), "with", step.getStepModel().RawWith, &decoded.With); err != nil {
|
||||
return err
|
||||
}
|
||||
step.getStepModel().With = decoded.With
|
||||
mergeIntoMap(step, step.getEnv(), decoded.GetEnv())
|
||||
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 mergeEnv(ctx context.Context, step step) {
|
||||
env := step.getEnv()
|
||||
rc := step.getRunContext()
|
||||
job := rc.Run.Job()
|
||||
|
||||
c := job.Container()
|
||||
if c != nil {
|
||||
// container env is the image's baseline, which job env and $GITHUB_ENV override
|
||||
mergeIntoMap(step, env, c.Env, rc.GetEnv())
|
||||
} else {
|
||||
mergeIntoMap(step, env, rc.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, step.getGithubContext(ctx), *env)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -58,13 +57,8 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
sar.Step.Uses = uses
|
||||
|
||||
github := sar.getGithubContext(ctx) // read before remoteAction is set, so `$/` resolves against the enclosing action
|
||||
if strings.HasPrefix(sar.Step.Uses, selfRepoPrefix) {
|
||||
sar.remoteAction = newSelfRepoAction(sar.Step.Uses, github)
|
||||
} else {
|
||||
sar.remoteAction = newRemoteAction(sar.Step.Uses)
|
||||
}
|
||||
if sar.remoteAction == nil {
|
||||
return fmt.Errorf("expected format {org}/{repo}[/path]@ref or %s{path}. Actual '%s' Input string was not in a correct format", selfRepoPrefix, sar.Step.Uses)
|
||||
if sar.remoteAction, err = newRemoteAction(sar.Step.Uses, github, sar.RunContext.Config.GitHubInstance); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
||||
@@ -164,11 +158,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||
common.Logger(ctx).Debugf("Skipping local actions/checkout because you bound your workspace")
|
||||
return nil
|
||||
}
|
||||
checkoutPath, err := sar.RunContext.NewExpressionEvaluator(ctx).Interpolate(ctx, sar.Step.With["path"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate with.path: %w", err)
|
||||
}
|
||||
copyToPath := path.Join(sar.RunContext.JobContainer.ToContainerPath(sar.RunContext.Config.Workdir), checkoutPath)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -324,61 +314,34 @@ func (ra *remoteAction) IsCheckout() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// newSelfRepoAction resolves `$/{path}` against the enclosing composite action, falling back to the workflow's own repo and commit.
|
||||
func newSelfRepoAction(action string, github *model.GithubContext) *remoteAction {
|
||||
subPath := strings.TrimLeft(strings.TrimPrefix(action, selfRepoPrefix), "/")
|
||||
if subPath == "" || strings.Contains(subPath, "@") || path.Clean("/"+subPath) != "/"+subPath { // rooted, so a leading ".." is rejected too
|
||||
return nil
|
||||
// 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
|
||||
}
|
||||
repo, ref := github.ActionRepository, github.ActionRef
|
||||
if repo == "" || ref == "" {
|
||||
repo, ref = github.Repository, github.Sha
|
||||
}
|
||||
org, name, _ := strings.Cut(repo, "/")
|
||||
if org == "" || name == "" || ref == "" {
|
||||
return nil
|
||||
}
|
||||
return &remoteAction{URL: github.ServerURL, Org: org, Repo: name, Path: subPath, Ref: ref}
|
||||
}
|
||||
|
||||
func newRemoteAction(action string) *remoteAction {
|
||||
// support http(s)://host/owner/repo@v3
|
||||
for _, schema := range []string{"https://", "http://", "ssh://"} {
|
||||
if after, ok := strings.CutPrefix(action, schema); ok {
|
||||
splits := strings.SplitN(after, "/", 2)
|
||||
if len(splits) != 2 {
|
||||
return nil
|
||||
}
|
||||
ret := parseAction(splits[1])
|
||||
if ret == nil {
|
||||
return nil
|
||||
}
|
||||
ret.URL = schema + splits[0]
|
||||
return ret
|
||||
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.Org == "" || ra.Repo == "" || ra.Ref == "" {
|
||||
return nil, fmt.Errorf("unable to resolve %q without a repository and commit", action)
|
||||
}
|
||||
}
|
||||
|
||||
return parseAction(action)
|
||||
}
|
||||
|
||||
func parseAction(action string) *remoteAction {
|
||||
// GitHub's document[^] describes:
|
||||
// > We strongly recommend that you include the version of
|
||||
// > the action you are using by specifying a Git ref, SHA, or Docker tag number.
|
||||
// Actually, the workflow stops if there is the uses directive that hasn't @ref.
|
||||
// [^]: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`)
|
||||
matches := r.FindStringSubmatch(action)
|
||||
if len(matches) < 7 || matches[6] == "" {
|
||||
return nil
|
||||
}
|
||||
return &remoteAction{
|
||||
Org: matches[1],
|
||||
Repo: matches[2],
|
||||
Path: matches[4],
|
||||
Ref: matches[6],
|
||||
URL: "",
|
||||
}
|
||||
return ra, nil
|
||||
}
|
||||
|
||||
func safeFilename(s string) string {
|
||||
|
||||
@@ -216,7 +216,7 @@ func TestStepActionRemote(t *testing.T) {
|
||||
sarm.On("readAction", sar.Step, actionDirSuffix(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
|
||||
}
|
||||
if tt.mocks.run {
|
||||
sarm.On("runAction", sar, actionDirSuffix(sar.Step.UsesHash()), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError })
|
||||
sarm.On("runAction", sar, actionDirSuffix(sar.Step.UsesHash()), mustNewRemoteAction(t, sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError })
|
||||
|
||||
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
|
||||
|
||||
@@ -249,7 +249,7 @@ func TestStepActionRemote(t *testing.T) {
|
||||
RunContext: &RunContext{Config: &Config{ActionCacheDir: t.TempDir()}, Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}}, JobContainer: &jobContainerMock{}},
|
||||
action: &model.Action{Inputs: map[string]model.Input{"shared": {}}},
|
||||
env: map[string]string{},
|
||||
remoteAction: newRemoteAction("org/composite@v1"),
|
||||
remoteAction: mustNewRemoteAction(t, "org/composite@v1"),
|
||||
}
|
||||
|
||||
for _, value := range []string{"first", "second"} {
|
||||
@@ -464,7 +464,7 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
Step: tt.stepModel,
|
||||
action: tt.actionModel,
|
||||
// post only ever runs after prepareActionExecutor resolved the action
|
||||
remoteAction: newRemoteAction(tt.stepModel.Uses),
|
||||
remoteAction: mustNewRemoteAction(t, tt.stepModel.Uses),
|
||||
}
|
||||
sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
|
||||
|
||||
@@ -611,10 +611,21 @@ func Test_newRemoteAction(t *testing.T) {
|
||||
action: "ssh://gitea.com/onlyonesegment@main", // missing org/repo after the host
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
action: "self:owner/repo/sub@v1",
|
||||
want: &remoteAction{
|
||||
URL: "https://gitea.example.com",
|
||||
Org: "owner",
|
||||
Repo: "repo",
|
||||
Path: "sub",
|
||||
Ref: "v1",
|
||||
},
|
||||
wantCloneURL: "https://gitea.example.com/owner/repo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.action, func(t *testing.T) {
|
||||
got := newRemoteAction(tt.action)
|
||||
got, _ := newRemoteAction(tt.action, nil, "gitea.example.com")
|
||||
assert.Equalf(t, tt.want, got, "newRemoteAction(%v)", tt.action)
|
||||
cloneURL := ""
|
||||
if got != nil {
|
||||
@@ -623,9 +634,18 @@ func Test_newRemoteAction(t *testing.T) {
|
||||
assert.Equalf(t, tt.wantCloneURL, cloneURL, "newRemoteAction(%v).CloneURL()", tt.action)
|
||||
})
|
||||
}
|
||||
_, err := newRemoteAction("self:owner/repo@v1", nil, "")
|
||||
require.ErrorContains(t, err, "without a Gitea instance")
|
||||
}
|
||||
|
||||
func Test_newSelfRepoAction(t *testing.T) {
|
||||
func mustNewRemoteAction(t *testing.T, uses string) *remoteAction {
|
||||
t.Helper()
|
||||
action, err := newRemoteAction(uses, nil, "")
|
||||
require.NoError(t, err)
|
||||
return action
|
||||
}
|
||||
|
||||
func Test_newRemoteActionSelfRepo(t *testing.T) {
|
||||
workflow := &model.GithubContext{
|
||||
ServerURL: "https://gitea.example.com",
|
||||
Repository: "owner/workflow-repo",
|
||||
@@ -674,7 +694,8 @@ func Test_newSelfRepoAction(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, newSelfRepoAction(tt.action, tt.github))
|
||||
got, _ := newRemoteAction(tt.action, tt.github, "")
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -704,7 +725,7 @@ func Test_remoteActionReference(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.uses, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, newRemoteAction(tt.uses).Reference())
|
||||
assert.Equal(t, tt.want, mustNewRemoteAction(t, tt.uses).Reference())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -761,7 +782,7 @@ func TestStepActionRemotePreResolvesDownloadedCommit(t *testing.T) {
|
||||
func TestStepActionRemoteActionDownloadInfo(t *testing.T) {
|
||||
t.Run("reports the action and its resolved commit", func(t *testing.T) {
|
||||
sar := &stepActionRemote{
|
||||
remoteAction: newRemoteAction("actions/checkout@v7"),
|
||||
remoteAction: mustNewRemoteAction(t, "actions/checkout@v7"),
|
||||
action: &model.Action{},
|
||||
resolvedSha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
}
|
||||
@@ -775,7 +796,7 @@ func TestStepActionRemoteActionDownloadInfo(t *testing.T) {
|
||||
|
||||
t.Run("reports nothing when no action was downloaded", func(t *testing.T) {
|
||||
// The local checkout of the workflow's own repository resolves no action.
|
||||
sar := &stepActionRemote{remoteAction: newRemoteAction("actions/checkout@v7")}
|
||||
sar := &stepActionRemote{remoteAction: mustNewRemoteAction(t, "actions/checkout@v7")}
|
||||
|
||||
_, _, ok := sar.actionDownloadInfo()
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -58,29 +57,17 @@ func (sd *stepDocker) runUsesContainer() common.Executor {
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
image := strings.TrimPrefix(step.Uses, "docker://")
|
||||
eval := rc.NewExpressionEvaluator(ctx)
|
||||
args, err := eval.Interpolate(ctx, step.With["args"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate with.args: %w", err)
|
||||
}
|
||||
cmd, err := shellquote.Split(args)
|
||||
cmd, err := shellquote.Split(step.With["args"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var entrypoint []string
|
||||
entry, err := eval.Interpolate(ctx, step.With["entrypoint"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to interpolate with.entrypoint: %w", err)
|
||||
}
|
||||
if entry != "" {
|
||||
if entry := step.With["entrypoint"]; entry != "" {
|
||||
entrypoint = []string{entry}
|
||||
}
|
||||
|
||||
stepContainer, err := newStepContainer(ctx, sd, image, cmd, entrypoint, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stepContainer := newStepContainer(ctx, sd, image, cmd, entrypoint, "")
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
stepContainer.Pull(rc.Config.ForcePull),
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
|
||||
}
|
||||
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||
|
||||
_, _ = newStepContainer(ctx, sd, "node:14", []string{"echo", "hi"}, nil, "")
|
||||
newStepContainer(ctx, sd, "node:14", []string{"echo", "hi"}, nil, "")
|
||||
assert.Equal(t, tc.allocPTY, captured.AllocatePTY)
|
||||
})
|
||||
}
|
||||
@@ -214,7 +214,7 @@ func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
|
||||
require.NoError(t, sd.RunContext.resolvePlatformImage(ctx))
|
||||
assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(), "IsHostEnv mismatch for platform %q", tc.platform)
|
||||
|
||||
_, _ = newStepContainer(ctx, sd, "alpine:3.20", []string{"echo", "hello"}, nil, "")
|
||||
newStepContainer(ctx, sd, "alpine:3.20", []string{"echo", "hello"}, nil, "")
|
||||
|
||||
if tc.expectDefault {
|
||||
assert.Equal(t, "default", captured.NetworkMode,
|
||||
|
||||
+24
-13
@@ -149,11 +149,8 @@ func TestSetupEnv(t *testing.T) {
|
||||
},
|
||||
JobContainer: cm,
|
||||
}
|
||||
step := &model.Step{
|
||||
With: map[string]string{
|
||||
"STEP_WITH": "with-value",
|
||||
},
|
||||
}
|
||||
step := &model.Step{}
|
||||
require.NoError(t, step.RawWith.Encode(map[string]string{"STEP_WITH": "with-value", "ID": "${{ fromJSON('1234567') }}"}))
|
||||
env := map[string]string{}
|
||||
|
||||
sm.On("getRunContext").Return(rc)
|
||||
@@ -162,6 +159,7 @@ func TestSetupEnv(t *testing.T) {
|
||||
sm.On("getEnv").Return(&env)
|
||||
|
||||
require.NoError(t, setupEnv(context.Background(), sm))
|
||||
require.NoError(t, setupInputs(context.Background(), sm))
|
||||
|
||||
// These are commit or system specific
|
||||
delete(env, "GITHUB_REF")
|
||||
@@ -198,6 +196,7 @@ func TestSetupEnv(t *testing.T) {
|
||||
"GITHUB_RUN_NUMBER": "1",
|
||||
"GITHUB_SERVER_URL": "https://",
|
||||
"GITHUB_WORKFLOW": "",
|
||||
"INPUT_ID": "1234567",
|
||||
"INPUT_STEP_WITH": "with-value",
|
||||
"RC_KEY": "rcvalue",
|
||||
"RUNNER_ENVIRONMENT": "self-hosted",
|
||||
@@ -214,7 +213,13 @@ env: ${{ fromJSON(matrix.env) }}
|
||||
jobs:
|
||||
test:
|
||||
env:
|
||||
PRIORITY: job
|
||||
PRIORITY: ${{ matrix.priority }}
|
||||
defaults:
|
||||
run: ${{ fromJSON(matrix.defaults) }}
|
||||
container:
|
||||
image: node:20
|
||||
env:
|
||||
CONTAINER: ${{ env.PRIORITY }}
|
||||
steps:
|
||||
- uses: ./act
|
||||
env:
|
||||
@@ -228,16 +233,22 @@ jobs:
|
||||
for _, value := range []string{"first", "second"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, map[string]any{"env": `{"WORKFLOW":"` + value + `","PRIORITY":"workflow"}`})
|
||||
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, map[string]any{"env": `{"WORKFLOW":"` + value + `${{ github.job }}","PRIORITY":"workflow"}`, "priority": "${{ github.job }}", "defaults": `{"shell":"` + value + `"}`})
|
||||
require.NoError(t, err)
|
||||
rc.workflowCallInputs = map[string]any{"args": `{"name":"` + value + `","fetch-depth":2}`}
|
||||
rc.workflowCallInputs = map[string]any{"args": `{"name":"` + value + `${{ github.job }}","fetch-depth":1234567}`}
|
||||
enabled, err := rc.isEnabled(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.True(t, enabled)
|
||||
require.NoError(t, evaluateJobEnvAndDefaults(t.Context(), rc))
|
||||
assert.Equal(t, model.RunDefaults{Shell: value}, rc.jobRunDefaults)
|
||||
step := &stepRun{RunContext: rc, Step: job.Steps[0].Clone(), env: map[string]string{}}
|
||||
require.NoError(t, setupEnv(t.Context(), step))
|
||||
assert.Equal(t, map[string]string{"ARGS": `${{ inputs.args }}`, "INPUT_NAME": value, "INPUT_FETCH-DEPTH": "2"}, step.Step.GetEnv())
|
||||
assert.Equal(t, value, step.env["INPUT_NAME"])
|
||||
assert.Equal(t, value, step.env["WORKFLOW"])
|
||||
assert.Equal(t, "job", step.env["PRIORITY"])
|
||||
require.NoError(t, setupInputs(t.Context(), step))
|
||||
assert.Equal(t, map[string]string{"ARGS": `${{ inputs.args }}`, "INPUT_NAME": value + "${{ github.job }}", "INPUT_FETCH-DEPTH": "1234567"}, step.Step.GetEnv())
|
||||
assert.Equal(t, value+"${{ github.job }}", step.env["INPUT_NAME"])
|
||||
assert.Equal(t, value+"${{ github.job }}", step.env["WORKFLOW"])
|
||||
assert.Equal(t, "${{ github.job }}", step.env["PRIORITY"])
|
||||
assert.Equal(t, "${{ github.job }}", step.env["CONTAINER"])
|
||||
assert.Equal(t, rawEnv, workflow.RawEnv)
|
||||
assert.Equal(t, rawWith, job.Steps[0].RawWith)
|
||||
assert.Nil(t, workflow.Env)
|
||||
@@ -253,7 +264,7 @@ jobs:
|
||||
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||
require.NoError(t, err)
|
||||
rc.workflowCallInputs = map[string]any{"args": "${{ inputs.unresolved }}"}
|
||||
require.ErrorContains(t, setupEnv(t.Context(), &stepRun{RunContext: rc, Step: workflow.Jobs["test"].Steps[0].Clone(), env: map[string]string{}}), "with:")
|
||||
require.ErrorContains(t, setupInputs(t.Context(), &stepRun{RunContext: rc, Step: workflow.Jobs["test"].Steps[0].Clone(), env: map[string]string{}}), "with:")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ toolchain go1.27.1
|
||||
require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
dario.cat/mergo v1.0.2
|
||||
gitea.dev/actionslib v1.0.0
|
||||
gitea.dev/actionslib v1.2.0
|
||||
github.com/avast/retry-go/v5 v5.0.0
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/creack/pty v1.1.24
|
||||
|
||||
@@ -4,8 +4,8 @@ cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik=
|
||||
cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
gitea.dev/actionslib v1.0.0 h1:l0oFJP+P4Ds1rlCI5zk618dYkuBc2mU7Gz5wPeG0lZY=
|
||||
gitea.dev/actionslib v1.0.0/go.mod h1:6O8YHkqVTKSR0LL2e5VhIDePYzGTZCbfmSVqJWEhk9g=
|
||||
gitea.dev/actionslib v1.2.0 h1:R/PBu9DwD04tCd5rjAWT9dAa+S6GOk1XnXCC3T2Rrnw=
|
||||
gitea.dev/actionslib v1.2.0/go.mod h1:1+gqOKGSEPn2IFHgY8S3GC5Ld+Hn8jF3OxiFHQ/fpx4=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
docker_container "github.com/moby/moby/api/types/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// CapabilityCancelling tells the server this runner understands the
|
||||
@@ -414,7 +415,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
|
||||
// Added per task because this job's service containers must be reached directly, and
|
||||
// act reaches them by their workflow key.
|
||||
proxyEnv := JobProxyEnv(envs, r.builtInCacheURL(), slices.Sorted(maps.Keys(job.Services)))
|
||||
var services map[string]yaml.Node
|
||||
_ = job.RawServices.Decode(&services) // a whole-value expression names no service before evaluation
|
||||
proxyEnv := JobProxyEnv(envs, r.builtInCacheURL(), slices.Sorted(maps.Keys(services)))
|
||||
maps.Copy(envs, proxyEnv)
|
||||
|
||||
if r.capabilities != "" {
|
||||
@@ -428,24 +431,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
r.getDefaultActionsURL(task),
|
||||
r.client.Address())
|
||||
|
||||
preset := &model.GithubContext{
|
||||
Event: taskContext["event"].GetStructValue().AsMap(),
|
||||
RunID: taskContext["run_id"].GetStringValue(),
|
||||
RunNumber: taskContext["run_number"].GetStringValue(),
|
||||
RunAttempt: taskContext["run_attempt"].GetStringValue(),
|
||||
Actor: taskContext["actor"].GetStringValue(),
|
||||
Repository: taskContext["repository"].GetStringValue(),
|
||||
EventName: taskContext["event_name"].GetStringValue(),
|
||||
Sha: taskContext["sha"].GetStringValue(),
|
||||
Ref: taskContext["ref"].GetStringValue(),
|
||||
RefName: taskContext["ref_name"].GetStringValue(),
|
||||
RefType: taskContext["ref_type"].GetStringValue(),
|
||||
HeadRef: taskContext["head_ref"].GetStringValue(),
|
||||
BaseRef: taskContext["base_ref"].GetStringValue(),
|
||||
Token: taskContext["token"].GetStringValue(),
|
||||
RepositoryOwner: taskContext["repository_owner"].GetStringValue(),
|
||||
RetentionDays: taskContext["retention_days"].GetStringValue(),
|
||||
}
|
||||
preset := model.GithubContextFromMap(task.Context.AsMap())
|
||||
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
||||
preset.Token = t
|
||||
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||
|
||||
Reference in new issue
Block a user