mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-13 03:13:42 +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 <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1233 Reviewed-by: bircni <[email protected]> Co-authored-by: silverwind <[email protected]>
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"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"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)
|
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eval := rc.NewActionInputsExpressionEvaluator(ctx, step)
|
cmd, err := shellquote.Split(step.getStepModel().With["args"])
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -375,14 +371,11 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entrypoint, err := dockerEntrypoint(ctx, step, eval, stage)
|
entrypoint, err := dockerEntrypoint(step, stage)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stepContainer, err := newStepContainer(ctx, step, image, cmd, entrypoint, rc.Config.ContainerOptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint, rc.Config.ContainerOptions)
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
prepImage,
|
prepImage,
|
||||||
stepContainer.Pull(forcePull),
|
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
|
// dockerEntrypoint returns the entrypoint the action's image runs with for the given
|
||||||
// stage. Only the main stage honours the `entrypoint` input.
|
// 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
|
runs := step.getActionModel().Runs
|
||||||
|
|
||||||
var entrypoint string
|
var entrypoint string
|
||||||
@@ -406,11 +399,7 @@ func dockerEntrypoint(ctx context.Context, step actionStep, eval *expressionEval
|
|||||||
default:
|
default:
|
||||||
entrypoint = runs.Entrypoint
|
entrypoint = runs.Entrypoint
|
||||||
if entrypoint == "" {
|
if entrypoint == "" {
|
||||||
withEntrypoint, err := eval.Interpolate(ctx, step.getStepModel().With["entrypoint"])
|
if fields := strings.Fields(step.getStepModel().With["entrypoint"]); len(fields) > 0 {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to interpolate with.entrypoint: %w", err)
|
|
||||||
}
|
|
||||||
if fields := strings.Fields(withEntrypoint); len(fields) > 0 {
|
|
||||||
return fields, nil
|
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.
|
// evalDockerEnv returns an evaluator bound to the environment it installed.
|
||||||
func evalDockerEnv(ctx context.Context, step step, action *model.Action) (*expressionEvaluator, error) {
|
func evalDockerEnv(ctx context.Context, step step, action *model.Action) (*expressionEvaluator, error) {
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
inputs := make(map[string]string)
|
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)
|
return nil, fmt.Errorf("unable to interpolate the default of input %s: %w", k, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, v := range stepModel.With {
|
mergeIntoMap(step, step.getEnv(), inputs, step.getStepModel().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
|
|
||||||
|
|
||||||
ee := rc.NewActionInputsExpressionEvaluator(ctx, step)
|
ee := rc.NewActionInputsExpressionEvaluator(ctx, step)
|
||||||
for k, v := range *step.getEnv() {
|
runsEnv := make(map[string]string, len(action.Runs.Env))
|
||||||
if (*step.getEnv())[k], err = ee.Interpolate(ctx, v); err != nil {
|
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)
|
return nil, fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mergeIntoMap(step, step.getEnv(), runsEnv, maps.Clone(*step.getEnv()))
|
||||||
return ee, nil
|
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()
|
rc := step.getRunContext()
|
||||||
logWriter := rc.commandLogWriter(ctx)
|
logWriter := rc.commandLogWriter(ctx)
|
||||||
envList := make([]string, 0)
|
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)...)
|
envList = append(envList, rc.runnerEnv(ctx)...)
|
||||||
|
|
||||||
binds, mounts, err := rc.GetBindsAndMounts()
|
binds, mounts := rc.GetBindsAndMounts()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
networkMode := "container:" + rc.jobContainerName()
|
networkMode := "container:" + rc.jobContainerName()
|
||||||
if rc.IsHostEnv() {
|
if rc.IsHostEnv() {
|
||||||
networkMode = "default"
|
networkMode = "default"
|
||||||
@@ -493,7 +471,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
|||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
ValidVolumes: rc.validVolumes(),
|
ValidVolumes: rc.validVolumes(),
|
||||||
AllocatePTY: rc.Config.AllocatePTY,
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
}), nil
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
|
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,
|
Parent: parent,
|
||||||
EventJSON: parent.EventJSON,
|
EventJSON: parent.EventJSON,
|
||||||
platformImage: parent.platformImage,
|
platformImage: parent.platformImage,
|
||||||
|
jobIndex: parent.jobIndex,
|
||||||
|
jobTotal: parent.jobTotal,
|
||||||
}
|
}
|
||||||
compositerc.setCompositeActionEnv(env)
|
compositerc.setCompositeActionEnv(env)
|
||||||
compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
|
compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
|
||||||
|
|||||||
@@ -12,18 +12,21 @@ import (
|
|||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCompositeActionParity(t *testing.T) {
|
func TestCompositeActionParity(t *testing.T) {
|
||||||
t.Run("inherits contexts without leaking inputs", func(t *testing.T) {
|
t.Run("inherits contexts without leaking inputs", func(t *testing.T) {
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
strategy := &model.Strategy{MaxParallelString: "3"}
|
strategy := &model.Strategy{MaxParallelString: "3", RawMatrix: yaml.Node{Kind: yaml.MappingNode}}
|
||||||
parent := &RunContext{
|
parent := &RunContext{
|
||||||
Config: &Config{},
|
Config: &Config{},
|
||||||
Matrix: map[string]any{"os": "linux"},
|
Matrix: map[string]any{"os": "linux"},
|
||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Name: "workflow", Jobs: map[string]*model.Job{"job": {Strategy: strategy}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Name: "workflow", Jobs: map[string]*model.Job{"job": {Strategy: strategy}}}},
|
||||||
JobContainer: &jobContainerMock{},
|
JobContainer: &jobContainerMock{},
|
||||||
platformImage: "-self-hosted",
|
platformImage: "-self-hosted",
|
||||||
|
jobIndex: 1,
|
||||||
|
jobTotal: 4,
|
||||||
}
|
}
|
||||||
composite, err := newCompositeRunContext(ctx, parent, &stepActionRemote{
|
composite, err := newCompositeRunContext(ctx, parent, &stepActionRemote{
|
||||||
Step: &model.Step{With: map[string]string{"SHARED": "outer"}},
|
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.Same(t, strategy, composite.Run.Job().Strategy)
|
||||||
assert.True(t, composite.IsHostEnv())
|
assert.True(t, composite.IsHostEnv())
|
||||||
interpolated, err := composite.NewExpressionEvaluator(ctx).Interpolate(ctx,
|
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)
|
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")
|
assert.NotContains(t, composite.Env, "INPUT_SHARED")
|
||||||
|
|
||||||
nestedEnv := composite.GetEnv()
|
nestedEnv := composite.GetEnv()
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ func TestActionRunner(t *testing.T) {
|
|||||||
|
|
||||||
tt.step.getRunContext().JobContainer = cm
|
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
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
@@ -316,7 +316,7 @@ func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
|
|||||||
step.On("getStepModel").Return(&model.Step{ID: "action"})
|
step.On("getStepModel").Return(&model.Step{ID: "action"})
|
||||||
step.On("getEnv").Return(&env)
|
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.
|
// DOCKER_USERNAME/DOCKER_PASSWORD should not be injected as pull credentials for docker action containers.
|
||||||
assert.Empty(t, captured.Username)
|
assert.Empty(t, captured.Username)
|
||||||
@@ -461,18 +461,11 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
|||||||
case <-innerEntered:
|
case <-innerEntered:
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
t.Fatalf("execAsDocker returned without entering inner: %v", err)
|
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()
|
cancel()
|
||||||
releaseOnce()
|
releaseOnce()
|
||||||
|
<-done
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatal("execAsDocker did not return after inner was released and ctx was canceled")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDockerActionImageTag(t *testing.T) {
|
func TestDockerActionImageTag(t *testing.T) {
|
||||||
|
|||||||
+38
-116
@@ -13,7 +13,6 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -27,29 +26,6 @@ import (
|
|||||||
"go.yaml.in/yaml/v4"
|
"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
|
// NewExpressionEvaluator creates a new evaluator
|
||||||
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) *ExpressionEvaluator {
|
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) *ExpressionEvaluator {
|
||||||
return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
|
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
|
var workflowCallResult map[string]*model.WorkflowCallResult
|
||||||
|
|
||||||
// todo: cleanup EvaluationEnvironment creation
|
// todo: cleanup EvaluationEnvironment creation
|
||||||
using := make(map[string]exprparser.Needs)
|
|
||||||
strategy := make(map[string]any)
|
strategy := make(map[string]any)
|
||||||
if rc.Run != nil {
|
if rc.Run != nil {
|
||||||
job := rc.Run.Job()
|
strategy = exprparser.StrategyContext(rc.Run.Job().Strategy, rc.jobIndex, rc.jobTotal)
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only setup jobs context in case of workflow_call
|
// only setup jobs context in case of workflow_call
|
||||||
// and existing expression evaluator (this means, jobs are at
|
// 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 {
|
if rc.caller != nil && rc.ExprEval != nil {
|
||||||
workflowCallResult = map[string]*model.WorkflowCallResult{}
|
workflowCallResult = map[string]*model.WorkflowCallResult{}
|
||||||
|
|
||||||
for jobName, job := range jobs {
|
for jobName, job := range rc.Run.Workflow.Jobs {
|
||||||
result := model.WorkflowCallResult{
|
result := model.WorkflowCallResult{
|
||||||
Outputs: map[string]string{},
|
Outputs: map[string]string{},
|
||||||
}
|
}
|
||||||
@@ -108,7 +70,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
|||||||
Vars: getWorkflowVars(ctx, rc),
|
Vars: getWorkflowVars(ctx, rc),
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
Matrix: rc.Matrix,
|
Matrix: rc.Matrix,
|
||||||
Needs: using,
|
Needs: exprparser.NeedsContext(rc.Run),
|
||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
HashFiles: getHashFilesFunction(ctx, rc),
|
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 {
|
func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, stepInputs map[string]any, jobContext *model.JobContext) *ExpressionEvaluator {
|
||||||
// todo: cleanup EvaluationEnvironment creation
|
// 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{
|
ee := &exprparser.EvaluationEnvironment{
|
||||||
Github: step.getGithubContext(ctx),
|
Github: step.getGithubContext(ctx),
|
||||||
Env: *step.getEnv(),
|
Env: *step.getEnv(),
|
||||||
@@ -161,9 +106,9 @@ func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step,
|
|||||||
Steps: rc.getStepsContext(),
|
Steps: rc.getStepsContext(),
|
||||||
Secrets: getWorkflowSecrets(rc),
|
Secrets: getWorkflowSecrets(rc),
|
||||||
Vars: getWorkflowVars(ctx, rc),
|
Vars: getWorkflowVars(ctx, rc),
|
||||||
Strategy: strategy,
|
Strategy: exprparser.StrategyContext(rc.Run.Job().Strategy, rc.jobIndex, rc.jobTotal),
|
||||||
Matrix: rc.Matrix,
|
Matrix: rc.Matrix,
|
||||||
Needs: using,
|
Needs: exprparser.NeedsContext(rc.Run),
|
||||||
// todo: should be unavailable
|
// todo: should be unavailable
|
||||||
// but required to interpolate/evaluate the inputs in actions/composite
|
// but required to interpolate/evaluate the inputs in actions/composite
|
||||||
Inputs: getEvaluatorInputs(rc, stepInputs, rc.getGithubContext(ctx)),
|
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, rc.workflowCallInputs)
|
||||||
maps.Copy(inputs, stepInputs)
|
maps.Copy(inputs, stepInputs)
|
||||||
|
|
||||||
if ghc.EventName == "workflow_dispatch" {
|
switch ghc.EventName {
|
||||||
config := rc.Run.Workflow.WorkflowDispatchConfig()
|
case "workflow_dispatch":
|
||||||
if config != nil && config.Inputs != nil {
|
if config := rc.Run.Workflow.WorkflowDispatchConfig(); config != nil {
|
||||||
for k, v := range config.Inputs {
|
for name, input := range config.Inputs {
|
||||||
value := nestedMapLookup(ghc.Event, "inputs", k)
|
if input.Type == "number" {
|
||||||
if value == nil {
|
input.Type = "string" // boolean is the only typed dispatch input, as in Gitea
|
||||||
value = v.Default
|
|
||||||
}
|
}
|
||||||
inputs[k] = coerceInputValue(value, v.Type)
|
setEvaluatorInput(inputs, ghc, name, model.WorkflowCallInput{Type: input.Type, Default: input.Default})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
case "workflow_call":
|
||||||
|
for name, input := range rc.Run.Workflow.WorkflowCallConfig().Inputs {
|
||||||
if ghc.EventName == "workflow_call" {
|
setEvaluatorInput(inputs, ghc, name, input)
|
||||||
config := rc.Run.Workflow.WorkflowCallConfig()
|
|
||||||
if config != nil && config.Inputs != nil {
|
|
||||||
for k, v := range config.Inputs {
|
|
||||||
value := nestedMapLookup(ghc.Event, "inputs", k)
|
|
||||||
if value == nil {
|
|
||||||
value = v.Default
|
|
||||||
}
|
|
||||||
inputs[k] = coerceInputValue(value, v.Type)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return inputs
|
return inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
// coerceInputValue converts an input value to the type declared by the workflow.
|
func setEvaluatorInput(inputs map[string]any, ghc *model.GithubContext, name string, input model.WorkflowCallInput) {
|
||||||
// The event payload carries natively typed JSON values on newer Gitea versions,
|
value := model.NestedMapLookup(ghc.Event, "inputs", name)
|
||||||
// while defaults and older servers provide strings.
|
if value == nil {
|
||||||
func coerceInputValue(value any, inputType string) any {
|
value, _ = input.DefaultValue()
|
||||||
if inputType != "boolean" {
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
if b, ok := value.(bool); ok {
|
inputs[name] = model.CoerceInputValue(value, input.Type)
|
||||||
return b
|
|
||||||
}
|
|
||||||
return value == "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveWorkflowCall evaluates the caller's with: and secrets: once, as the server does before dispatching a called workflow.
|
// 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 {
|
if rc.caller == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
logger := common.Logger(ctx)
|
||||||
callerJob := rc.caller.runContext.Run.Job()
|
callerJob := rc.caller.runContext.Run.Job()
|
||||||
callerEval := rc.caller.runContext.ExprEval
|
callerEval := rc.caller.runContext.ExprEval.shared(ctx)
|
||||||
callerWith := callerJob.With
|
var callerWith map[string]any
|
||||||
if err := decodeDeferred(ctx, callerEval, "workflow inputs", callerJob.RawWith, &callerWith); err != nil {
|
if err := model.DecodeEvaluated("workflow inputs", callerJob.RawWith, callerEval.EvaluateYamlNode, &callerWith); err != nil {
|
||||||
return err
|
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()
|
config := rc.Run.Workflow.WorkflowCallConfig()
|
||||||
|
var err error
|
||||||
rc.workflowCallInputs = make(map[string]any, len(config.Inputs))
|
if rc.workflowCallInputs, err = callerEval.ResolveWorkflowCallInputs(config, callerWith); err != nil {
|
||||||
for name, input := range config.Inputs {
|
logger.Warn(err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if callerJob.InheritSecrets() {
|
||||||
|
rc.workflowCallSecrets = getWorkflowSecrets(rc.caller.runContext)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
secrets := callerJob.Secrets()
|
secrets := callerJob.Secrets()
|
||||||
if secrets == nil && callerJob.InheritSecrets() {
|
if err := config.ValidateSecrets(secrets); err != nil {
|
||||||
secrets = rc.caller.runContext.Config.Secrets
|
logger.Warn(err)
|
||||||
}
|
}
|
||||||
rc.workflowCallSecrets = make(map[string]string, len(secrets))
|
if rc.workflowCallSecrets, err = callerEval.EvaluateCallerSecrets(secrets); err != nil {
|
||||||
for k, v := range secrets {
|
return fmt.Errorf("unable to interpolate %w", err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ on:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
default: gitea
|
default: gitea
|
||||||
|
unset:
|
||||||
|
type: boolean
|
||||||
`,
|
`,
|
||||||
"workflow_dispatch": `
|
"workflow_dispatch": `
|
||||||
on:
|
on:
|
||||||
@@ -305,6 +307,8 @@ on:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
default: gitea
|
default: gitea
|
||||||
|
unset:
|
||||||
|
type: boolean
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +360,7 @@ on:
|
|||||||
inputs := getEvaluatorInputs(rc, nil, ghc)
|
inputs := getEvaluatorInputs(rc, nil, ghc)
|
||||||
assert.Equal(t, table.flag, inputs["flag"])
|
assert.Equal(t, table.flag, inputs["flag"])
|
||||||
assert.Equal(t, "gitea", inputs["name"])
|
assert.Equal(t, "gitea", inputs["name"])
|
||||||
|
assert.Equal(t, false, inputs["unset"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,11 +380,11 @@ jobs:
|
|||||||
for _, value := range []string{"true", "false"} {
|
for _, value := range []string{"true", "false"} {
|
||||||
t.Run(value, func(t *testing.T) {
|
t.Run(value, func(t *testing.T) {
|
||||||
t.Parallel()
|
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)
|
require.NoError(t, err)
|
||||||
child, err := (&runnerImpl{config: &Config{}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
child, err := (&runnerImpl{config: &Config{}, caller: &caller{runContext: parent}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||||
require.NoError(t, err)
|
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.Equal(t, rawWith, job.RawWith)
|
||||||
assert.Nil(t, job.With)
|
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) {
|
func TestJobNameMasksSecrets(t *testing.T) {
|
||||||
workflow, err := model.ReadWorkflow(strings.NewReader(`
|
workflow, err := model.ReadWorkflow(strings.NewReader(`
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
+20
-25
@@ -18,7 +18,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -414,49 +413,45 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
|||||||
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
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 {
|
func evaluateJobEnvAndDefaults(ctx context.Context, rc *RunContext) error {
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
|
env := rc.GetEnv()
|
||||||
var workflowEnv map[string]string
|
var workflowEnv map[string]string
|
||||||
if err := decodeDeferred(ctx, rc.ExprEval, "workflow env", rc.Run.Workflow.RawEnv, &workflowEnv); err != nil {
|
if rc.Run.Workflow.Env == nil {
|
||||||
return err
|
if err := model.DecodeEvaluated("workflow env", rc.Run.Workflow.RawEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &workflowEnv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if workflowEnv != nil {
|
if workflowEnv != nil {
|
||||||
rc.Env = mergeMaps(workflowEnv, rc.GetEnv())
|
rc.Env = mergeMaps(workflowEnv, env)
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
}
|
}
|
||||||
var err error
|
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 {
|
if rc.Env[k], err = rc.ExprEval.Interpolate(ctx, v); err != nil {
|
||||||
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaults := rc.Run.Job().Defaults.Run
|
var defaults model.Defaults
|
||||||
if rc.jobRunDefaults.Shell, err = rc.ExprEval.Interpolate(ctx, defaults.Shell); err != nil {
|
if err := model.DecodeEvaluated("defaults", rc.Run.Job().RawDefaults, rc.ExprEval.shared(ctx).EvaluateYamlNode, &defaults); err != nil {
|
||||||
return fmt.Errorf("unable to interpolate defaults.run.shell: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
if rc.jobRunDefaults.WorkingDirectory, err = rc.ExprEval.Interpolate(ctx, defaults.WorkingDirectory); err != nil {
|
rc.jobRunDefaults = defaults.Run
|
||||||
return fmt.Errorf("unable to interpolate defaults.run.working-directory: %w", err)
|
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 {
|
func setJobOutputs(ctx context.Context, rc *RunContext) error {
|
||||||
if rc.caller == nil {
|
if rc.caller == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
outputs := rc.Run.Workflow.WorkflowCallConfig().Outputs
|
callerOutputs, err := rc.NewExpressionEvaluator(ctx).shared(ctx).EvaluateWorkflowCallOutputs(rc.Run.Workflow.WorkflowCallConfig())
|
||||||
callerOutputs := make(map[string]string, len(outputs))
|
if err != nil {
|
||||||
ee := sync.OnceValue(func() *expressionEvaluator { return rc.NewExpressionEvaluator(ctx) })
|
return fmt.Errorf("unable to interpolate workflow %w", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matrix combinations of a reusable-workflow caller share the caller's *model.Job;
|
// 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 {
|
if err != nil {
|
||||||
common.Logger(ctx).Errorf("An error occurred when attempting to determine the job timeout: %s", err)
|
common.Logger(ctx).Errorf("An error occurred when attempting to determine the job timeout: %s", err)
|
||||||
} else if timeout != "" {
|
} 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)
|
return context.WithTimeout(ctx, time.Duration(timeoutMinutes)*time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,8 +479,7 @@ func TestNewJobExecutor(t *testing.T) {
|
|||||||
Config: &Config{},
|
Config: &Config{},
|
||||||
}
|
}
|
||||||
if tt.output != "" {
|
if tt.output != "" {
|
||||||
rc.Run.Job().Outputs = map[string]string{"bad": tt.output}
|
require.NoError(t, rc.Run.Job().RawOutputs.Encode(map[string]string{"bad": tt.output}))
|
||||||
rc.outputTemplate = map[string]string{"bad": tt.output}
|
|
||||||
}
|
}
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
executorOrder := make([]string, 0)
|
executorOrder := make([]string, 0)
|
||||||
@@ -1082,6 +1081,7 @@ func TestApplyJobTimeout(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"empty", "runs-on: ubuntu-latest", false},
|
{"empty", "runs-on: ubuntu-latest", false},
|
||||||
{"integer", "timeout-minutes: 5\nruns-on: ubuntu-latest", true},
|
{"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},
|
{"non-numeric ignored", "timeout-minutes: abc\nruns-on: ubuntu-latest", false},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -51,11 +50,7 @@ func (rc *RunContext) runJobHook(ctx context.Context, hookPath, name string) err
|
|||||||
rawLogger.Infof("shell: %s", shell)
|
rawLogger.Infof("shell: %s", shell)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := map[string]string{}
|
env := mergeMaps(rc.containerSpec.Env, rc.GetEnv())
|
||||||
if jobContainer := rc.Run.Job().Container(); jobContainer != nil {
|
|
||||||
maps.Copy(env, jobContainer.Env)
|
|
||||||
}
|
|
||||||
maps.Copy(env, rc.GetEnv())
|
|
||||||
rc.withGithubEnv(ctx, rc.getGithubContext(ctx), env)
|
rc.withGithubEnv(ctx, rc.getGithubContext(ctx), env)
|
||||||
rc.ApplyExtraPath(ctx, &env)
|
rc.ApplyExtraPath(ctx, &env)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -20,28 +19,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||||
if !rc.Config.NoSkipCheckout {
|
localWorkflow, err := model.ParseReusableWorkflowUses(rc.Run.Job().Uses)
|
||||||
fullPath := rc.Run.Job().Uses
|
if err != nil {
|
||||||
|
return common.NewErrorExecutor(err)
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ./.gitea/workflows/wf.yml -> .gitea/workflows/wf.yml
|
if !rc.Config.NoSkipCheckout {
|
||||||
trimmedUses := strings.TrimPrefix(rc.Run.Job().Uses, "./")
|
// resolve the local workflow against the workspace root, not the process
|
||||||
// uses string format is {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}
|
// working directory, so it is found regardless of where the runner is invoked
|
||||||
uses := fmt.Sprintf("%s/%s@%s", rc.Config.PresetGitHubContext.Repository, trimmedUses, rc.Config.PresetGitHubContext.Sha)
|
return newReusableWorkflowExecutor(rc, rc.Config.Workdir, localWorkflow.Path)
|
||||||
|
}
|
||||||
|
|
||||||
remoteReusableWorkflow := newRemoteReusableWorkflowWithPlat(rc.Config.GitHubInstance, uses)
|
uses := fmt.Sprintf("%s/%s@%s", rc.Config.PresetGitHubContext.Repository, localWorkflow.Path, rc.Config.PresetGitHubContext.Sha)
|
||||||
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, uses)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorExecutor(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(uses))
|
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(uses))
|
||||||
@@ -51,37 +44,27 @@ func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
|||||||
|
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
||||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.FilePath()),
|
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||||
uses := rc.Run.Job().Uses
|
remoteReusableWorkflow, err := newRemoteReusableWorkflow(rc.Config.GitHubInstance, rc.Run.Job().Uses)
|
||||||
|
if err != nil {
|
||||||
var remoteReusableWorkflow *remoteReusableWorkflow
|
return common.NewErrorExecutor(err)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// uses with safe filename makes the target directory look something like this {owner}-{repo}-.github-workflows-{filename}@{ref}
|
// 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
|
// 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
|
// 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))
|
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename))
|
||||||
|
|
||||||
token := getGitCloneToken(rc.Config, remoteReusableWorkflow.CloneURL())
|
token := getGitCloneToken(rc.Config, remoteReusableWorkflow.CloneURL())
|
||||||
|
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
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:
|
// Callers must not change remoteReusableWorkflow.URL, because:
|
||||||
// 1. Gitea doesn't support specifying GithubContext.ServerURL by the GITHUB_SERVER_URL env
|
// 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
|
// remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
|
||||||
func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory, token string) common.Executor {
|
func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory, token string) common.Executor {
|
||||||
@@ -153,64 +136,34 @@ func newReusableWorkflowRunner(rc *RunContext) (*runnerImpl, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type remoteReusableWorkflow struct {
|
type remoteReusableWorkflow struct {
|
||||||
URL string
|
*model.ReusableWorkflowUses
|
||||||
Org string
|
URL string
|
||||||
Repo string
|
|
||||||
Filename string
|
|
||||||
Ref string
|
|
||||||
|
|
||||||
GitPlatform string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *remoteReusableWorkflow) CloneURL() 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.
|
// 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://") {
|
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 {
|
var absoluteReusableWorkflowURLRegex = regexp.MustCompile(`^(https?://.*)/([^/]+/[^/]+/\.[^/]+/workflows/[^@]+@.*)$`)
|
||||||
return fmt.Sprintf("./.%s/workflows/%s", r.GitPlatform, r.Filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For Gitea
|
// For Gitea
|
||||||
// newRemoteReusableWorkflowWithPlat create a `remoteReusableWorkflow`
|
// newRemoteReusableWorkflow parses a remote `uses`, an absolute URL up to `{owner}/{repo}/.{git_platform}/workflows/` replaces baseURL
|
||||||
// workflows from `.gitea/workflows` and `.github/workflows` are supported
|
func newRemoteReusableWorkflow(baseURL, uses string) (*remoteReusableWorkflow, error) {
|
||||||
func newRemoteReusableWorkflowWithPlat(url, uses string) *remoteReusableWorkflow {
|
if matches := absoluteReusableWorkflowURLRegex.FindStringSubmatch(uses); matches != nil {
|
||||||
// GitHub docs:
|
baseURL, uses = matches[1], matches[2]
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
return &remoteReusableWorkflow{
|
parsed, err := model.ParseReusableWorkflowUses(uses)
|
||||||
Org: matches[1],
|
if err != nil {
|
||||||
Repo: matches[2],
|
return nil, err
|
||||||
GitPlatform: matches[3],
|
|
||||||
Filename: matches[4],
|
|
||||||
Ref: matches[5],
|
|
||||||
URL: url,
|
|
||||||
}
|
}
|
||||||
}
|
if parsed.IsLocal() {
|
||||||
|
return nil, fmt.Errorf("expected a remote reusable workflow, got %q", uses)
|
||||||
// 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],
|
|
||||||
}
|
}
|
||||||
|
return &remoteReusableWorkflow{ReusableWorkflowUses: parsed, URL: baseURL}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Gitea
|
// For Gitea
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"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) {
|
func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) {
|
||||||
conf := &Config{
|
conf := &Config{
|
||||||
GitHubInstance: "gitea.example.net",
|
GitHubInstance: "gitea.example.net",
|
||||||
|
|||||||
+75
-197
@@ -35,8 +35,10 @@ import (
|
|||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/kballard/go-shellquote"
|
||||||
"github.com/moby/moby/api/types/mount"
|
"github.com/moby/moby/api/types/mount"
|
||||||
"github.com/opencontainers/selinux/go-selinux"
|
"github.com/opencontainers/selinux/go-selinux"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"golang.org/x/text/encoding/unicode"
|
"golang.org/x/text/encoding/unicode"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
@@ -62,6 +64,7 @@ type RunContext struct {
|
|||||||
ExprEval *expressionEvaluator
|
ExprEval *expressionEvaluator
|
||||||
JobContainer container.ExecutionsEnvironment
|
JobContainer container.ExecutionsEnvironment
|
||||||
serviceContainers []*serviceContainer
|
serviceContainers []*serviceContainer
|
||||||
|
containerSpec model.ContainerSpec // container:, resolved once with platformImage, zero without a job container
|
||||||
JobName string
|
JobName string
|
||||||
ActionPath string
|
ActionPath string
|
||||||
Parent *RunContext
|
Parent *RunContext
|
||||||
@@ -74,6 +77,7 @@ type RunContext struct {
|
|||||||
workflowCallSecrets map[string]string // the caller's secrets:, resolved once by resolveWorkflowCall
|
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
|
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
|
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)
|
// 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
|
// 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
|
// 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.
|
// 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.
|
// Only populated on the top-level RunContext; child RCs walk Parent via topLevelRunContext.
|
||||||
summaryFileInitialized map[int]bool
|
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
|
// jobCancelled records that this job's run was cancelled (context.Canceled). It makes
|
||||||
// getJobContext report the "cancelled" status so cancelled()/always() evaluate the way
|
// getJobContext report the "cancelled" status so cancelled()/always() evaluate the way
|
||||||
// GitHub Actions does, letting cleanup and always() steps run while normal steps skip.
|
// 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
|
// 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()
|
name := rc.jobContainerName()
|
||||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
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
|
// 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"] {
|
if rc.containerDaemonSocket() != "-" && !claimed["/var/run/docker.sock"] {
|
||||||
binds = append(binds, rc.jobDockerSocket()+":/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 {
|
func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||||
@@ -486,7 +472,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
image := rc.platformImage
|
image := rc.platformImage
|
||||||
logWriter := rc.commandLogWriter(ctx)
|
logWriter := rc.commandLogWriter(ctx)
|
||||||
|
|
||||||
username, password, err := rc.handleCredentials(ctx)
|
username, password, err := registryCredentials(rc.containerSpec.Credentials, "container.")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to handle credentials: %s", err)
|
return fmt.Errorf("failed to handle credentials: %s", err)
|
||||||
}
|
}
|
||||||
@@ -507,78 +493,57 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
networkName, createAndDeleteNetwork := rc.networkNameForGitea()
|
networkName, createAndDeleteNetwork := rc.networkNameForGitea()
|
||||||
rc.cleanUpJobContainer = rc.cleanupJobResources(networkName, createAndDeleteNetwork, false)
|
rc.cleanUpJobContainer = rc.cleanupJobResources(networkName, createAndDeleteNetwork, false)
|
||||||
|
|
||||||
services := rc.Run.Job().Services
|
var services map[string]*model.ContainerSpec
|
||||||
if err := decodeDeferred(ctx, rc.ExprEval, "job services", rc.Run.Job().RawServices, &services); err != nil {
|
if err := model.DecodeEvaluated("job services", rc.Run.Job().RawServices, rc.ExprEval.shared(ctx).EvaluateYamlNode, &services); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for serviceID, spec := range services {
|
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
|
// GitHub compatibility: skip services whose image evaluates to an
|
||||||
// empty string, enabling conditional services via expressions
|
// empty string, enabling conditional services via expressions
|
||||||
serviceImage, err := rc.ExprEval.Interpolate(ctx, spec.Image)
|
if spec.Image == "" {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to interpolate service %s image: %w", serviceID, err)
|
|
||||||
}
|
|
||||||
if serviceImage == "" {
|
|
||||||
logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
|
logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// interpolate env
|
envs := make([]string, 0, len(spec.Env)+len(rc.Config.ProxyEnv))
|
||||||
interpolatedEnvs := make(map[string]string, len(spec.Env)+len(rc.Config.ProxyEnv))
|
|
||||||
// a service reaches the internet the way the job does; its own env still wins
|
// 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 mergeMaps(rc.Config.ProxyEnv, spec.Env) {
|
||||||
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 {
|
|
||||||
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
|
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
|
// keep these local: reusing username/password would overwrite the
|
||||||
// credentials the job container is pulled with further down
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
|
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
|
||||||
}
|
}
|
||||||
|
cmd := spec.Cmd
|
||||||
interpolatedVolumes := make([]string, len(spec.Volumes))
|
if len(cmd) == 0 && spec.Command != "" {
|
||||||
for i, volume := range spec.Volumes {
|
if cmd, err = shellquote.Split(spec.Command); err != nil {
|
||||||
if interpolatedVolumes[i], err = rc.ExprEval.Interpolate(ctx, volume); err != nil {
|
return fmt.Errorf("failed to parse service %s command: %w", serviceID, err)
|
||||||
return fmt.Errorf("unable to interpolate service %s volumes: %w", serviceID, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serviceBinds, serviceMounts, _ := splitVolumes(interpolatedVolumes)
|
var entrypoint []string
|
||||||
|
if spec.Entrypoint != "" {
|
||||||
interpolatedPorts := make([]string, len(spec.Ports))
|
entrypoint = []string{spec.Entrypoint}
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts)
|
|
||||||
|
serviceBinds, serviceMounts, _ := splitVolumes(spec.Volumes)
|
||||||
|
|
||||||
|
exposedPorts, portBindings, err := nat.ParsePortSpecs(spec.Ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse service %s ports: %w", serviceID, err)
|
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)
|
serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
|
||||||
c := newContainer(&container.NewContainerInput{
|
c := newContainer(&container.NewContainerInput{
|
||||||
Name: serviceContainerName,
|
Name: serviceContainerName,
|
||||||
Image: serviceImage,
|
Image: spec.Image,
|
||||||
Username: serviceUsername,
|
Username: serviceUsername,
|
||||||
Password: servicePassword,
|
Password: servicePassword,
|
||||||
Cmd: interpolatedCmd,
|
Entrypoint: entrypoint,
|
||||||
|
Cmd: cmd,
|
||||||
Env: envs,
|
Env: envs,
|
||||||
Mounts: serviceMounts,
|
Mounts: serviceMounts,
|
||||||
Binds: serviceBinds,
|
Binds: serviceBinds,
|
||||||
@@ -588,7 +553,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
UsernsMode: rc.Config.UsernsMode,
|
UsernsMode: rc.Config.UsernsMode,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
AutoRemove: false, // so a dead service's log survives, cleanupJobResources removes it
|
AutoRemove: false, // so a dead service's log survives, cleanupJobResources removes it
|
||||||
WorkflowOptions: serviceOptions,
|
WorkflowOptions: spec.Options,
|
||||||
NetworkMode: networkName,
|
NetworkMode: networkName,
|
||||||
NetworkAliases: []string{serviceID},
|
NetworkAliases: []string{serviceID},
|
||||||
ExposedPorts: exposedPorts,
|
ExposedPorts: exposedPorts,
|
||||||
@@ -596,16 +561,12 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
ValidVolumes: rc.Config.ValidVolumes, // not validVolumes(), a service gets no docker socket
|
ValidVolumes: rc.Config.ValidVolumes, // not validVolumes(), a service gets no docker socket
|
||||||
AllocatePTY: rc.Config.AllocatePTY,
|
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`
|
// For Gitea, `jobContainerNetwork` should be the same as `networkName`
|
||||||
jobContainerNetwork := networkName
|
jobContainerNetwork := networkName
|
||||||
|
|
||||||
workflowOptions, err := rc.workflowOptions(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||||
containerInput := &container.NewContainerInput{
|
containerInput := &container.NewContainerInput{
|
||||||
Cmd: nil,
|
Cmd: nil,
|
||||||
@@ -624,7 +585,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
UsernsMode: rc.Config.UsernsMode,
|
UsernsMode: rc.Config.UsernsMode,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
RunnerOptions: rc.Config.ContainerOptions,
|
RunnerOptions: rc.Config.ContainerOptions,
|
||||||
WorkflowOptions: workflowOptions,
|
WorkflowOptions: rc.containerSpec.Options,
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
AllocatePTY: rc.Config.AllocatePTY,
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
}
|
}
|
||||||
@@ -641,9 +602,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc.startDockerProxy(ctx)
|
rc.startDockerProxy(ctx)
|
||||||
if containerInput.Binds, containerInput.Mounts, err = rc.GetBindsAndMounts(); err != nil {
|
containerInput.Binds, containerInput.Mounts = rc.GetBindsAndMounts()
|
||||||
return err
|
|
||||||
}
|
|
||||||
containerInput.ValidVolumes = rc.validVolumes()
|
containerInput.ValidVolumes = rc.validVolumes()
|
||||||
|
|
||||||
rc.jobNetworkName = networkName
|
rc.jobNetworkName = networkName
|
||||||
@@ -1033,39 +992,16 @@ func lockJob(job *model.Job) func() {
|
|||||||
|
|
||||||
func (rc *RunContext) interpolateOutputs() common.Executor {
|
func (rc *RunContext) interpolateOutputs() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
ee := rc.NewExpressionEvaluator(ctx)
|
|
||||||
job := rc.Run.Job()
|
job := rc.Run.Job()
|
||||||
// Matrix combinations share this Job and its Outputs map. Interpolate from this combo's
|
var outputs map[string]string
|
||||||
// pristine snapshot (outputTemplate) and write under the lock, so each combo overwrites
|
if err := model.DecodeEvaluated("job outputs", job.RawOutputs, rc.NewExpressionEvaluator(ctx).shared(ctx).EvaluateYamlNode, &outputs); err != nil {
|
||||||
// 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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if deferredOutputs != nil {
|
// Matrix combinations share the job, so as on GitHub a failure publishes nothing and the last non-empty value wins.
|
||||||
defer lockJob(job)()
|
maps0.DeleteFunc(outputs, func(_, value string) bool { return value == "" })
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer lockJob(job)()
|
defer lockJob(job)()
|
||||||
for k := range rc.outputTemplate {
|
job.Outputs = mergeMaps(job.Outputs, outputs)
|
||||||
if err != nil {
|
return nil
|
||||||
job.Outputs[k] = ""
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
job.Outputs[k] = outputs[k]
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,18 +1120,6 @@ func (rc *RunContext) Executor() (common.Executor, error) {
|
|||||||
}, nil
|
}, 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) {
|
func (rc *RunContext) runsOnImage(ctx context.Context) (string, error) {
|
||||||
if rc.Run.Job().RunsOn() == nil {
|
if rc.Run.Job().RunsOn() == nil {
|
||||||
common.Logger(ctx).Errorf("'runs-on' key not defined in %s", rc.String())
|
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)
|
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 {
|
func (rc *RunContext) resolvePlatformImage(ctx context.Context) error {
|
||||||
image, err := rc.containerImage(ctx)
|
withoutEnv, _ := splitContainerEnv(rc.Run.Job().RawContainer)
|
||||||
if err == nil && image == "" {
|
var spec *model.ContainerSpec
|
||||||
image, err = rc.runsOnImage(ctx)
|
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
|
rc.platformImage = image
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) workflowOptions(ctx context.Context) (string, error) {
|
// splitContainerEnv takes `env` out of a container mapping, since env reads the job env that resolves after the job environment starts.
|
||||||
c := rc.Run.Job().Container()
|
func splitContainerEnv(container yaml.Node) (withoutEnv, env yaml.Node) {
|
||||||
if c == nil {
|
if container.Kind != yaml.MappingNode {
|
||||||
return "", nil
|
return container, env
|
||||||
}
|
}
|
||||||
options, err := rc.ExprEval.Interpolate(ctx, c.Options)
|
withoutEnv = container
|
||||||
if err != nil {
|
withoutEnv.Content = nil
|
||||||
return "", fmt.Errorf("unable to interpolate container.options: %w", err)
|
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) {
|
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
|
{ // Adapt to Gitea
|
||||||
if preset := rc.Config.PresetGitHubContext; preset != nil {
|
if preset := rc.Config.PresetGitHubContext; preset != nil {
|
||||||
ghc.Event = preset.Event
|
merged := *preset
|
||||||
ghc.RunID = preset.RunID
|
merged.Workflow, merged.Job, merged.Workspace, merged.EventPath = ghc.Workflow, ghc.Job, ghc.Workspace, ghc.EventPath
|
||||||
ghc.RunNumber = preset.RunNumber
|
merged.Action, merged.ActionPath, merged.ActionRepository, merged.ActionRef = ghc.Action, ghc.ActionPath, ghc.ActionRepository, ghc.ActionRef
|
||||||
ghc.RunAttempt = preset.RunAttempt
|
merged.RunnerPerflog, merged.RunnerTrackingID = ghc.RunnerPerflog, ghc.RunnerTrackingID
|
||||||
ghc.Actor = preset.Actor
|
*ghc = merged
|
||||||
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
|
|
||||||
|
|
||||||
instance := rc.Config.GitHubInstance
|
instance := rc.Config.GitHubInstance
|
||||||
if !strings.HasPrefix(instance, "http://") &&
|
if !strings.HasPrefix(instance, "http://") &&
|
||||||
@@ -1563,12 +1487,7 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
|
|||||||
if step.Type() != model.StepTypeUsesActionRemote {
|
if step.Type() != model.StepTypeUsesActionRemote {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
remoteAction := newRemoteAction(step.Uses)
|
if uses, err := model.ParseActionUses(step.Uses); err != nil || uses.Owner != "actions" || uses.Repo != "checkout" {
|
||||||
if remoteAction == nil {
|
|
||||||
// IsCheckout() will nil panic if we dont bail out early
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !remoteAction.IsCheckout() {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1581,23 +1500,6 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
|
|||||||
return true
|
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) {
|
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) {
|
||||||
env["CI"] = "true"
|
env["CI"] = "true"
|
||||||
env["GITHUB_WORKFLOW"] = github.Workflow
|
env["GITHUB_WORKFLOW"] = github.Workflow
|
||||||
@@ -1721,41 +1623,17 @@ func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
|||||||
env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken
|
env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) handleCredentials(ctx context.Context) (string, string, error) {
|
func registryCredentials(credentials map[string]string, prefix string) (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) {
|
|
||||||
if credentials == nil {
|
if credentials == nil {
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
if len(credentials) != 2 {
|
if len(credentials) != 2 {
|
||||||
return "", "", errors.New("invalid property count for key 'credentials:'")
|
return "", "", errors.New("invalid property count for key 'credentials:'")
|
||||||
}
|
}
|
||||||
|
for _, key := range []string{"username", "password"} {
|
||||||
ee := rc.NewExpressionEvaluator(ctx)
|
if credentials[key] == "" {
|
||||||
interpolate := func(key string) (string, error) {
|
return "", "", fmt.Errorf("failed to interpolate %scredentials.%s", prefix, key)
|
||||||
value, err := ee.Interpolate(ctx, credentials[key])
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to interpolate %scredentials.%s: %w", prefix, key, err)
|
|
||||||
}
|
}
|
||||||
if value == "" {
|
|
||||||
return "", fmt.Errorf("failed to interpolate %scredentials.%s", prefix, key)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
}
|
||||||
username, err := interpolate("username")
|
return credentials["username"], credentials["password"], nil
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
password, err := interpolate("password")
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return username, 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
|
// 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
|
// 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.
|
// 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.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.Binds) // the docker socket is the job container's alone
|
||||||
require.Empty(t, redis.WorkingDir)
|
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 TestStartJobContainerEvaluatesContainersOnce(t *testing.T) {
|
||||||
func TestStartJobContainerGivesServicesTheirVolumesFromExpression(t *testing.T) {
|
inputs := startJobContainerInputs(t, `
|
||||||
redis := startJobContainerInputs(t, `
|
|
||||||
jobs:
|
jobs:
|
||||||
job:
|
job:
|
||||||
services: ${{ fromJSON('{"redis":{"image":"redis:latest","volumes":["data:/data"]}}') }}
|
container: ${{ fromJSON('{"image":"node:20","options":"--label ${{ github.job }}","credentials":{"username":"user","password":"${{ vars.TITLE }}"}}') }}
|
||||||
`, &Config{ValidVolumes: []string{"data"}})[0]
|
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, "redis:latest", redis.Image)
|
||||||
require.Equal(t, []string{"data"}, redis.ValidVolumes)
|
|
||||||
require.Equal(t, map[string]string{"data": "/data"}, redis.Mounts)
|
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
|
// 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 := testcase.rc.Config
|
||||||
config.Workdir = testcase.name
|
config.Workdir = testcase.name
|
||||||
config.BindWorkdir = bindWorkDir
|
config.BindWorkdir = bindWorkDir
|
||||||
gotbind, gotmount, err := rctemplate.GetBindsAndMounts()
|
gotbind, gotmount := rctemplate.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Name binds/mounts are either/or
|
// Name binds/mounts are either/or
|
||||||
if config.BindWorkdir {
|
if config.BindWorkdir {
|
||||||
@@ -543,8 +519,9 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("InterpolatedContainerVolumes", func(t *testing.T) {
|
t.Run("InterpolatedContainerVolumes", func(t *testing.T) {
|
||||||
job := &model.Job{}
|
job := &model.Job{}
|
||||||
err := job.RawContainer.Encode(map[string][]string{
|
err := job.RawContainer.Encode(map[string]any{
|
||||||
"volumes": {"${{ secrets.MAME }}:/root/.mame/roms:ro"},
|
"image": "node:20",
|
||||||
|
"volumes": []string{"${{ secrets.MAME }}:/root/.mame/roms:ro"},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -565,9 +542,9 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
rc.Run.JobID = "job1"
|
rc.Run.JobID = "job1"
|
||||||
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}
|
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||||
|
require.NoError(t, rc.resolvePlatformImage(context.Background()))
|
||||||
|
|
||||||
gotbind, gotmount, err := rc.GetBindsAndMounts()
|
gotbind, gotmount := rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, gotbind, "/host/mame/roms:/root/.mame/roms:ro")
|
assert.Contains(t, gotbind, "/host/mame/roms:/root/.mame/roms:ro")
|
||||||
assert.NotContains(t, gotbind, "${{ secrets.MAME }}")
|
assert.NotContains(t, gotbind, "${{ secrets.MAME }}")
|
||||||
assert.NotContains(t, gotmount, "${{ secrets.MAME }}")
|
assert.NotContains(t, gotmount, "${{ secrets.MAME }}")
|
||||||
@@ -575,12 +552,6 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
|
|
||||||
for _, testcase := range tests {
|
for _, testcase := range tests {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
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{
|
rc := &RunContext{
|
||||||
Name: "TestRCName",
|
Name: "TestRCName",
|
||||||
Run: &model.Run{
|
Run: &model.Run{
|
||||||
@@ -592,12 +563,11 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
BindWorkdir: false,
|
BindWorkdir: false,
|
||||||
SharedToolCache: true, // so OverridesToolCache has a mount to displace
|
SharedToolCache: true, // so OverridesToolCache has a mount to displace
|
||||||
},
|
},
|
||||||
|
containerSpec: model.ContainerSpec{Volumes: testcase.volumes},
|
||||||
}
|
}
|
||||||
rc.Run.JobID = "job1"
|
rc.Run.JobID = "job1"
|
||||||
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}
|
|
||||||
|
|
||||||
gotbind, gotmount, err := rc.GetBindsAndMounts()
|
gotbind, gotmount := rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
if len(testcase.wantbind) > 0 {
|
if len(testcase.wantbind) > 0 {
|
||||||
assert.Contains(t, gotbind, testcase.wantbind)
|
assert.Contains(t, gotbind, testcase.wantbind)
|
||||||
@@ -632,13 +602,11 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
Config: &Config{},
|
Config: &Config{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, gotmount, err := rc.GetBindsAndMounts()
|
_, gotmount := rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotContains(t, gotmount, sharedToolCacheVolume)
|
assert.NotContains(t, gotmount, sharedToolCacheVolume)
|
||||||
|
|
||||||
rc.Config.SharedToolCache = true
|
rc.Config.SharedToolCache = true
|
||||||
_, gotmount, err = rc.GetBindsAndMounts()
|
_, gotmount = rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, container.DefaultToolCache, gotmount[sharedToolCacheVolume])
|
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{}},
|
Config: &Config{BindWorkdir: true, Workdir: "/workspace/1/owner/repo", PresetGitHubContext: &model.GithubContext{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
gotbind, _, err := rc.GetBindsAndMounts()
|
gotbind, _ := rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, slices.ContainsFunc(gotbind, func(bind string) bool { return strings.HasPrefix(bind, "/workspace/1:/workspace/1") }), gotbind)
|
assert.True(t, slices.ContainsFunc(gotbind, func(bind string) bool { return strings.HasPrefix(bind, "/workspace/1:/workspace/1") }), gotbind)
|
||||||
|
|
||||||
rc.Config.BindWorkdir = false
|
rc.Config.BindWorkdir = false
|
||||||
_, gotmount, err := rc.GetBindsAndMounts()
|
_, gotmount := rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "/workspace/1", gotmount[rc.jobContainerName()])
|
assert.Equal(t, "/workspace/1", gotmount[rc.jobContainerName()])
|
||||||
|
|
||||||
require.NoError(t, rc.Run.Job().RawContainer.Encode(map[string][]string{"volumes": {"claimed:/workspace/1"}}))
|
rc.containerSpec.Volumes = []string{"claimed:/workspace/1"}
|
||||||
_, gotmount, err = rc.GetBindsAndMounts()
|
_, gotmount = rc.GetBindsAndMounts()
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "/workspace/1/owner/repo", gotmount[rc.jobContainerName()])
|
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
|
func TestInterpolateOutputsIsPerMatrixComboKeepingNeedsOutputs(t *testing.T) {
|
||||||
// *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) {
|
|
||||||
job := &model.Job{Outputs: map[string]string{"o": "${{ matrix.v }}"}}
|
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{}}
|
r := &runnerImpl{config: &Config{}}
|
||||||
ctx := context.Background()
|
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"})
|
rcA, err := r.newRunContext(ctx, run, map[string]any{"v": "a"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rcB, err := r.newRunContext(ctx, run, map[string]any{"v": "b"})
|
rcB, err := r.newRunContext(ctx, run, map[string]any{"v": "b"})
|
||||||
require.NoError(t, err)
|
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, rcA.interpolateOutputs()(ctx))
|
||||||
require.NoError(t, rcB.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, map[string]string{"o": "b"}, job.Outputs)
|
||||||
require.Equal(t, "b", job.Outputs["o"])
|
require.Equal(t, map[string]string{"o": "from-gitea"}, needed.Outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A whole-value `outputs:` expression only reaches the typed field through DecodeRaw.
|
// 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.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.Token, rc.Config.Secrets["GITHUB_TOKEN"])
|
||||||
assert.Equal(t, ghc.Job, "job1") //nolint:testifylint // pre-existing issue from nektos/act
|
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) {
|
func TestGetGithubContextRef(t *testing.T) {
|
||||||
|
|||||||
+26
-9
@@ -7,7 +7,6 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -159,9 +158,9 @@ func (runner *runnerImpl) configure() (*runnerImpl, error) {
|
|||||||
func maxParallelFor(strategy *model.Strategy, combinations int) int {
|
func maxParallelFor(strategy *model.Strategy, combinations int) int {
|
||||||
maxParallel := 4 // actionslib has no default for an undeclared max-parallel
|
maxParallel := 4 // actionslib has no default for an undeclared max-parallel
|
||||||
if strategy != nil {
|
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)
|
log.Errorf("Ignoring invalid max-parallel: %v", err)
|
||||||
} else if declared {
|
} else if limit > 0 {
|
||||||
maxParallel = limit
|
maxParallel = limit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +209,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if job.RawStrategy.Kind == yaml.ScalarNode {
|
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
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil {
|
||||||
log.Errorf("Error while evaluating matrix: %v", err)
|
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))
|
log.Infof("Running job with maxParallel=%d for %d matrix combinations", maxParallel, len(matrixes))
|
||||||
|
|
||||||
for i, matrix := range 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 {
|
if err != nil {
|
||||||
return err
|
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) {
|
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{
|
rc := &RunContext{
|
||||||
Config: runner.config,
|
Config: runner.config,
|
||||||
Run: run,
|
Run: run,
|
||||||
@@ -332,16 +349,16 @@ func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, mat
|
|||||||
StepResults: make(map[string]*model.StepResult),
|
StepResults: make(map[string]*model.StepResult),
|
||||||
Matrix: matrix,
|
Matrix: matrix,
|
||||||
caller: runner.caller,
|
caller: runner.caller,
|
||||||
|
jobIndex: index,
|
||||||
|
jobTotal: total,
|
||||||
}
|
}
|
||||||
if err := rc.resolveWorkflowCall(ctx); err != nil {
|
if err := rc.resolveWorkflowCall(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
rc.Name = rc.maskSecrets(rc.ExprEval.InterpolateName(ctx, run.String()))
|
rc.Name = rc.maskSecrets(rc.ExprEval.InterpolateName(ctx, run.String()))
|
||||||
// Snapshot the job's pristine output expressions now, before any matrix combo runs and
|
if job := run.Job(); job != nil && job.RawOutputs.Kind != 0 {
|
||||||
// rewrites the shared Job.Outputs (see interpolateOutputs).
|
job.Outputs = nil // a job no combination ran for reads "" as on GitHub, a Gitea needs placeholder has no RawOutputs and keeps its outputs
|
||||||
if job := run.Job(); job != nil {
|
|
||||||
rc.outputTemplate = maps.Clone(job.Outputs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rc, nil
|
return rc, nil
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -29,6 +30,7 @@ var (
|
|||||||
logLevel = log.DebugLevel
|
logLevel = log.DebugLevel
|
||||||
workdir = "testdata"
|
workdir = "testdata"
|
||||||
secrets map[string]string
|
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 {
|
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,
|
EventPath: cfg.EventPath,
|
||||||
PlatformPicker: mapPlatformPicker(j.platforms),
|
PlatformPicker: mapPlatformPicker(j.platforms),
|
||||||
// fixtures reuse workflow and job names, so parallel tests would collide without this
|
// 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
|
// 0 would run jobs runtime.NumCPU()-wide, making the network peak machine-dependent
|
||||||
MaxParallel: 2,
|
MaxParallel: 2,
|
||||||
ForceRebuild: true,
|
ForceRebuild: true,
|
||||||
@@ -346,7 +348,7 @@ func TestRunEvent(t *testing.T) {
|
|||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
Secrets: table.secrets,
|
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")
|
eventFile := filepath.Join(workdir, table.workflowPath, "event.json")
|
||||||
|
|||||||
+22
-33
@@ -11,7 +11,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -19,7 +18,6 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/actionslib/pkg/exprparser"
|
"gitea.dev/actionslib/pkg/exprparser"
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"go.yaml.in/yaml/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type step interface {
|
type step interface {
|
||||||
@@ -89,6 +87,9 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
runStep, err = isStepEnabled(ctx, ifExpression, step, stage)
|
runStep, err = isStepEnabled(ctx, ifExpression, step, stage)
|
||||||
}
|
}
|
||||||
|
if err == nil && runStep {
|
||||||
|
err = setupInputs(ctx, step)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stepResult.Conclusion = model.StepStatusFailure
|
stepResult.Conclusion = model.StepStatusFailure
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
stepResult.Outcome = model.StepStatusFailure
|
||||||
@@ -240,51 +241,39 @@ func setupEnv(ctx context.Context, step step) error {
|
|||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
|
|
||||||
mergeEnv(ctx, step)
|
mergeEnv(ctx, step)
|
||||||
// merge step env last, since it should not be overwritten
|
|
||||||
mergeIntoMap(step, step.getEnv(), step.getStepModel().GetEnv())
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
exprEval := rc.NewExpressionEvaluator(ctx)
|
exprEval := rc.NewExpressionEvaluator(ctx)
|
||||||
for k, v := range *step.getEnv() {
|
stepEnv := step.getStepModel().Environment()
|
||||||
if !strings.HasPrefix(k, "INPUT_") {
|
for k, v := range stepEnv {
|
||||||
if (*step.getEnv())[k], err = exprEval.Interpolate(ctx, v); err != nil {
|
if stepEnv[k], err = exprEval.Interpolate(ctx, v); err != nil {
|
||||||
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
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
|
// merge step env last, since it should not be overwritten
|
||||||
// you can use step level env in the with property of a uses construct
|
mergeIntoMap(step, step.getEnv(), stepEnv)
|
||||||
inputEval := sync.OnceValue(func() *expressionEvaluator { return rc.NewExpressionEvaluatorWithEnv(ctx, *step.getEnv()) })
|
return nil
|
||||||
for k, v := range *step.getEnv() {
|
}
|
||||||
if strings.HasPrefix(k, "INPUT_") {
|
|
||||||
if (*step.getEnv())[k], err = inputEval().Interpolate(ctx, v); err != nil {
|
// setupInputs evaluates `with` once the condition passed, as GitHub does, so every consumer reads the evaluated With.
|
||||||
return fmt.Errorf("unable to interpolate env %s: %w", k, err)
|
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 {
|
if err := model.DecodeEvaluated("with", stepModel.RawWith, step.getRunContext().NewStepExpressionEvaluator(ctx, step).shared(ctx).EvaluateYamlNode, &stepModel.With); err != nil {
|
||||||
decoded := &model.Step{}
|
return err
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
mergeIntoMap(step, step.getEnv(), (&model.Step{With: stepModel.With}).GetEnv())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeEnv(ctx context.Context, step step) {
|
func mergeEnv(ctx context.Context, step step) {
|
||||||
env := step.getEnv()
|
env := step.getEnv()
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
job := rc.Run.Job()
|
|
||||||
|
|
||||||
c := job.Container()
|
// container env is the image's baseline, which job env and $GITHUB_ENV override
|
||||||
if c != nil {
|
mergeIntoMap(step, env, rc.containerSpec.Env, rc.GetEnv())
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env)
|
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *env)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -58,13 +57,8 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
sar.Step.Uses = uses
|
sar.Step.Uses = uses
|
||||||
|
|
||||||
github := sar.getGithubContext(ctx) // read before remoteAction is set, so `$/` resolves against the enclosing action
|
github := sar.getGithubContext(ctx) // read before remoteAction is set, so `$/` resolves against the enclosing action
|
||||||
if strings.HasPrefix(sar.Step.Uses, selfRepoPrefix) {
|
if sar.remoteAction, err = newRemoteAction(sar.Step.Uses, github, sar.RunContext.Config.GitHubInstance); err != nil {
|
||||||
sar.remoteAction = newSelfRepoAction(sar.Step.Uses, github)
|
return err
|
||||||
} 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.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
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")
|
common.Logger(ctx).Debugf("Skipping local actions/checkout because you bound your workspace")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
checkoutPath, err := sar.RunContext.NewExpressionEvaluator(ctx).Interpolate(ctx, sar.Step.With["path"])
|
copyToPath := path.Join(sar.RunContext.JobContainer.ToContainerPath(sar.RunContext.Config.Workdir), 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)
|
|
||||||
return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore, false)(ctx)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSelfRepoAction resolves `$/{path}` against the enclosing composite action, falling back to the workflow's own repo and commit.
|
// newRemoteAction resolves `self:` on instanceURL and `$/` in the enclosing composite action, else the workflow's repo and commit.
|
||||||
func newSelfRepoAction(action string, github *model.GithubContext) *remoteAction {
|
func newRemoteAction(action string, github *model.GithubContext, instanceURL string) (*remoteAction, error) {
|
||||||
subPath := strings.TrimLeft(strings.TrimPrefix(action, selfRepoPrefix), "/")
|
uses, err := model.ParseActionUses(action)
|
||||||
if subPath == "" || strings.Contains(subPath, "@") || path.Clean("/"+subPath) != "/"+subPath { // rooted, so a leading ".." is rejected too
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
repo, ref := github.ActionRepository, github.ActionRef
|
ra := &remoteAction{URL: uses.URL, Org: uses.Owner, Repo: uses.Repo, Path: uses.Path, Ref: uses.Ref}
|
||||||
if repo == "" || ref == "" {
|
switch uses.Kind {
|
||||||
repo, ref = github.Repository, github.Sha
|
case model.ActionUsesInstance:
|
||||||
}
|
if instanceURL == "" {
|
||||||
org, name, _ := strings.Cut(repo, "/")
|
return nil, fmt.Errorf("unable to resolve %q without a Gitea instance", action)
|
||||||
if org == "" || name == "" || ref == "" {
|
}
|
||||||
return nil
|
ra.URL = instanceURL
|
||||||
}
|
if !strings.Contains(instanceURL, "://") {
|
||||||
return &remoteAction{URL: github.ServerURL, Org: org, Repo: name, Path: subPath, Ref: ref}
|
ra.URL = "https://" + instanceURL
|
||||||
}
|
}
|
||||||
|
case model.ActionUsesSelfRepo:
|
||||||
func newRemoteAction(action string) *remoteAction {
|
repo, ref := github.ActionRepository, github.ActionRef
|
||||||
// support http(s)://host/owner/repo@v3
|
if repo == "" || ref == "" {
|
||||||
for _, schema := range []string{"https://", "http://", "ssh://"} {
|
repo, ref = github.Repository, github.Sha
|
||||||
if after, ok := strings.CutPrefix(action, schema); ok {
|
}
|
||||||
splits := strings.SplitN(after, "/", 2)
|
ra.Org, ra.Repo, _ = strings.Cut(repo, "/")
|
||||||
if len(splits) != 2 {
|
ra.URL, ra.Ref = github.ServerURL, ref
|
||||||
return nil
|
if ra.Org == "" || ra.Repo == "" || ra.Ref == "" {
|
||||||
}
|
return nil, fmt.Errorf("unable to resolve %q without a repository and commit", action)
|
||||||
ret := parseAction(splits[1])
|
|
||||||
if ret == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ret.URL = schema + splits[0]
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ra, nil
|
||||||
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: "",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func safeFilename(s string) string {
|
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)
|
sarm.On("readAction", sar.Step, actionDirSuffix(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
|
||||||
}
|
}
|
||||||
if tt.mocks.run {
|
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)
|
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{}},
|
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": {}}},
|
action: &model.Action{Inputs: map[string]model.Input{"shared": {}}},
|
||||||
env: map[string]string{},
|
env: map[string]string{},
|
||||||
remoteAction: newRemoteAction("org/composite@v1"),
|
remoteAction: mustNewRemoteAction(t, "org/composite@v1"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, value := range []string{"first", "second"} {
|
for _, value := range []string{"first", "second"} {
|
||||||
@@ -464,7 +464,7 @@ func TestStepActionRemotePost(t *testing.T) {
|
|||||||
Step: tt.stepModel,
|
Step: tt.stepModel,
|
||||||
action: tt.actionModel,
|
action: tt.actionModel,
|
||||||
// post only ever runs after prepareActionExecutor resolved the action
|
// 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)
|
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
|
action: "ssh://gitea.com/onlyonesegment@main", // missing org/repo after the host
|
||||||
want: nil,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.action, func(t *testing.T) {
|
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)
|
assert.Equalf(t, tt.want, got, "newRemoteAction(%v)", tt.action)
|
||||||
cloneURL := ""
|
cloneURL := ""
|
||||||
if got != nil {
|
if got != nil {
|
||||||
@@ -623,9 +634,18 @@ func Test_newRemoteAction(t *testing.T) {
|
|||||||
assert.Equalf(t, tt.wantCloneURL, cloneURL, "newRemoteAction(%v).CloneURL()", tt.action)
|
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{
|
workflow := &model.GithubContext{
|
||||||
ServerURL: "https://gitea.example.com",
|
ServerURL: "https://gitea.example.com",
|
||||||
Repository: "owner/workflow-repo",
|
Repository: "owner/workflow-repo",
|
||||||
@@ -674,7 +694,8 @@ func Test_newSelfRepoAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.uses, func(t *testing.T) {
|
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) {
|
func TestStepActionRemoteActionDownloadInfo(t *testing.T) {
|
||||||
t.Run("reports the action and its resolved commit", func(t *testing.T) {
|
t.Run("reports the action and its resolved commit", func(t *testing.T) {
|
||||||
sar := &stepActionRemote{
|
sar := &stepActionRemote{
|
||||||
remoteAction: newRemoteAction("actions/checkout@v7"),
|
remoteAction: mustNewRemoteAction(t, "actions/checkout@v7"),
|
||||||
action: &model.Action{},
|
action: &model.Action{},
|
||||||
resolvedSha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
resolvedSha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||||
}
|
}
|
||||||
@@ -775,7 +796,7 @@ func TestStepActionRemoteActionDownloadInfo(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("reports nothing when no action was downloaded", func(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.
|
// 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()
|
_, _, ok := sar.actionDownloadInfo()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -58,29 +57,17 @@ func (sd *stepDocker) runUsesContainer() common.Executor {
|
|||||||
|
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
image := strings.TrimPrefix(step.Uses, "docker://")
|
image := strings.TrimPrefix(step.Uses, "docker://")
|
||||||
eval := rc.NewExpressionEvaluator(ctx)
|
cmd, err := shellquote.Split(step.With["args"])
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var entrypoint []string
|
var entrypoint []string
|
||||||
entry, err := eval.Interpolate(ctx, step.With["entrypoint"])
|
if entry := step.With["entrypoint"]; entry != "" {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to interpolate with.entrypoint: %w", err)
|
|
||||||
}
|
|
||||||
if entry != "" {
|
|
||||||
entrypoint = []string{entry}
|
entrypoint = []string{entry}
|
||||||
}
|
}
|
||||||
|
|
||||||
stepContainer, err := newStepContainer(ctx, sd, image, cmd, entrypoint, "")
|
stepContainer := newStepContainer(ctx, sd, image, cmd, entrypoint, "")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
stepContainer.Pull(rc.Config.ForcePull),
|
stepContainer.Pull(rc.Config.ForcePull),
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
|
|||||||
}
|
}
|
||||||
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
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)
|
assert.Equal(t, tc.allocPTY, captured.AllocatePTY)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
|
|||||||
require.NoError(t, sd.RunContext.resolvePlatformImage(ctx))
|
require.NoError(t, sd.RunContext.resolvePlatformImage(ctx))
|
||||||
assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(), "IsHostEnv mismatch for platform %q", tc.platform)
|
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 {
|
if tc.expectDefault {
|
||||||
assert.Equal(t, "default", captured.NetworkMode,
|
assert.Equal(t, "default", captured.NetworkMode,
|
||||||
|
|||||||
+24
-13
@@ -149,11 +149,8 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
},
|
},
|
||||||
JobContainer: cm,
|
JobContainer: cm,
|
||||||
}
|
}
|
||||||
step := &model.Step{
|
step := &model.Step{}
|
||||||
With: map[string]string{
|
require.NoError(t, step.RawWith.Encode(map[string]string{"STEP_WITH": "with-value", "ID": "${{ fromJSON('1234567') }}"}))
|
||||||
"STEP_WITH": "with-value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
env := map[string]string{}
|
env := map[string]string{}
|
||||||
|
|
||||||
sm.On("getRunContext").Return(rc)
|
sm.On("getRunContext").Return(rc)
|
||||||
@@ -162,6 +159,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
sm.On("getEnv").Return(&env)
|
sm.On("getEnv").Return(&env)
|
||||||
|
|
||||||
require.NoError(t, setupEnv(context.Background(), sm))
|
require.NoError(t, setupEnv(context.Background(), sm))
|
||||||
|
require.NoError(t, setupInputs(context.Background(), sm))
|
||||||
|
|
||||||
// These are commit or system specific
|
// These are commit or system specific
|
||||||
delete(env, "GITHUB_REF")
|
delete(env, "GITHUB_REF")
|
||||||
@@ -198,6 +196,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
"GITHUB_RUN_NUMBER": "1",
|
"GITHUB_RUN_NUMBER": "1",
|
||||||
"GITHUB_SERVER_URL": "https://",
|
"GITHUB_SERVER_URL": "https://",
|
||||||
"GITHUB_WORKFLOW": "",
|
"GITHUB_WORKFLOW": "",
|
||||||
|
"INPUT_ID": "1234567",
|
||||||
"INPUT_STEP_WITH": "with-value",
|
"INPUT_STEP_WITH": "with-value",
|
||||||
"RC_KEY": "rcvalue",
|
"RC_KEY": "rcvalue",
|
||||||
"RUNNER_ENVIRONMENT": "self-hosted",
|
"RUNNER_ENVIRONMENT": "self-hosted",
|
||||||
@@ -214,7 +213,13 @@ env: ${{ fromJSON(matrix.env) }}
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
env:
|
env:
|
||||||
PRIORITY: job
|
PRIORITY: ${{ matrix.priority }}
|
||||||
|
defaults:
|
||||||
|
run: ${{ fromJSON(matrix.defaults) }}
|
||||||
|
container:
|
||||||
|
image: node:20
|
||||||
|
env:
|
||||||
|
CONTAINER: ${{ env.PRIORITY }}
|
||||||
steps:
|
steps:
|
||||||
- uses: ./act
|
- uses: ./act
|
||||||
env:
|
env:
|
||||||
@@ -228,16 +233,22 @@ jobs:
|
|||||||
for _, value := range []string{"first", "second"} {
|
for _, value := range []string{"first", "second"} {
|
||||||
t.Run(value, func(t *testing.T) {
|
t.Run(value, func(t *testing.T) {
|
||||||
t.Parallel()
|
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)
|
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))
|
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{}}
|
step := &stepRun{RunContext: rc, Step: job.Steps[0].Clone(), env: map[string]string{}}
|
||||||
require.NoError(t, setupEnv(t.Context(), step))
|
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())
|
require.NoError(t, setupInputs(t.Context(), step))
|
||||||
assert.Equal(t, value, step.env["INPUT_NAME"])
|
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, step.env["WORKFLOW"])
|
assert.Equal(t, value+"${{ github.job }}", step.env["INPUT_NAME"])
|
||||||
assert.Equal(t, "job", step.env["PRIORITY"])
|
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, rawEnv, workflow.RawEnv)
|
||||||
assert.Equal(t, rawWith, job.Steps[0].RawWith)
|
assert.Equal(t, rawWith, job.Steps[0].RawWith)
|
||||||
assert.Nil(t, workflow.Env)
|
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)
|
rc, err := (&runnerImpl{config: &Config{}}).newRunContext(t.Context(), &model.Run{Workflow: workflow, JobID: "test"}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rc.workflowCallInputs = map[string]any{"args": "${{ inputs.unresolved }}"}
|
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 (
|
require (
|
||||||
connectrpc.com/connect v1.21.0
|
connectrpc.com/connect v1.21.0
|
||||||
dario.cat/mergo v1.0.2
|
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/avast/retry-go/v5 v5.0.0
|
||||||
github.com/containerd/errdefs v1.0.0
|
github.com/containerd/errdefs v1.0.0
|
||||||
github.com/creack/pty v1.1.24
|
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=
|
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 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
gitea.dev/actionslib v1.0.0 h1:l0oFJP+P4Ds1rlCI5zk618dYkuBc2mU7Gz5wPeG0lZY=
|
gitea.dev/actionslib v1.2.0 h1:R/PBu9DwD04tCd5rjAWT9dAa+S6GOk1XnXCC3T2Rrnw=
|
||||||
gitea.dev/actionslib v1.0.0/go.mod h1:6O8YHkqVTKSR0LL2e5VhIDePYzGTZCbfmSVqJWEhk9g=
|
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 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
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=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import (
|
|||||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
docker_container "github.com/moby/moby/api/types/container"
|
docker_container "github.com/moby/moby/api/types/container"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CapabilityCancelling tells the server this runner understands the
|
// 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
|
// Added per task because this job's service containers must be reached directly, and
|
||||||
// act reaches them by their workflow key.
|
// 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)
|
maps.Copy(envs, proxyEnv)
|
||||||
|
|
||||||
if r.capabilities != "" {
|
if r.capabilities != "" {
|
||||||
@@ -428,24 +431,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
r.getDefaultActionsURL(task),
|
r.getDefaultActionsURL(task),
|
||||||
r.client.Address())
|
r.client.Address())
|
||||||
|
|
||||||
preset := &model.GithubContext{
|
preset := model.GithubContextFromMap(task.Context.AsMap())
|
||||||
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(),
|
|
||||||
}
|
|
||||||
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
if t := task.Secrets["GITEA_TOKEN"]; t != "" {
|
||||||
preset.Token = t
|
preset.Token = t
|
||||||
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||||
|
|||||||
Reference in new issue
Block a user