mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-06 00:44:22 +02:00
Compare commits
2 Commits
c3b39e0d99
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc0e03e5a9 | ||
|
|
333eb17d19 |
@@ -84,7 +84,9 @@ type ActionRuns struct {
|
|||||||
Post string `yaml:"post"`
|
Post string `yaml:"post"`
|
||||||
PostIf string `yaml:"post-if"`
|
PostIf string `yaml:"post-if"`
|
||||||
Image string `yaml:"image"`
|
Image string `yaml:"image"`
|
||||||
|
PreEntrypoint string `yaml:"pre-entrypoint"`
|
||||||
Entrypoint string `yaml:"entrypoint"`
|
Entrypoint string `yaml:"entrypoint"`
|
||||||
|
PostEntrypoint string `yaml:"post-entrypoint"`
|
||||||
Args []string `yaml:"args"`
|
Args []string `yaml:"args"`
|
||||||
Steps []Step `yaml:"steps"`
|
Steps []Step `yaml:"steps"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,3 +61,22 @@ runs:
|
|||||||
t.Fatalf("error = %q, want invalid value", err)
|
t.Fatalf("error = %q, want invalid value", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadActionDockerEntrypoints(t *testing.T) {
|
||||||
|
action, err := ReadAction(strings.NewReader(`
|
||||||
|
runs:
|
||||||
|
using: docker
|
||||||
|
image: Dockerfile
|
||||||
|
pre-entrypoint: pre.sh
|
||||||
|
post-entrypoint: post.sh
|
||||||
|
`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if action.Runs.PreEntrypoint != "pre.sh" {
|
||||||
|
t.Fatalf("pre-entrypoint = %q, want pre.sh", action.Runs.PreEntrypoint)
|
||||||
|
}
|
||||||
|
if action.Runs.PostEntrypoint != "post.sh" {
|
||||||
|
t.Fatalf("post-entrypoint = %q, want post.sh", action.Runs.PostEntrypoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
|||||||
if remoteAction == nil {
|
if remoteAction == nil {
|
||||||
location = containerActionDir
|
location = containerActionDir
|
||||||
}
|
}
|
||||||
return execAsDocker(ctx, step, actionName, actionDir, location, remoteAction == nil)
|
return execAsDocker(ctx, step, actionName, actionDir, location, remoteAction == nil, stepStageMain)
|
||||||
case x.IsComposite():
|
case x.IsComposite():
|
||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -305,7 +305,7 @@ func dockerActionImageTag(repository, actionName string, localAction bool) strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: break out parts of function to reduce complexicity
|
// TODO: break out parts of function to reduce complexicity
|
||||||
func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, basedir string, localAction bool) error {
|
func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, basedir string, localAction bool, stage stepStage) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
@@ -386,17 +386,10 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
|||||||
cmd = action.Runs.Args
|
cmd = action.Runs.Args
|
||||||
evalDockerArgs(ctx, step, action, &cmd)
|
evalDockerArgs(ctx, step, action, &cmd)
|
||||||
}
|
}
|
||||||
entrypoint := strings.Fields(eval.Interpolate(ctx, step.getStepModel().With["entrypoint"]))
|
entrypoint, err := dockerEntrypoint(ctx, step, eval, stage)
|
||||||
if len(entrypoint) == 0 {
|
|
||||||
if action.Runs.Entrypoint != "" {
|
|
||||||
entrypoint, err = shellquote.Split(action.Runs.Entrypoint)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
entrypoint = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
|
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
prepImage,
|
prepImage,
|
||||||
@@ -409,6 +402,30 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
|||||||
).Finally(stepContainer.Close())(ctx)
|
).Finally(stepContainer.Close())(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dockerEntrypoint returns the entrypoint the action's image runs with for the given
|
||||||
|
// stage. Only the main stage honours the `entrypoint` input.
|
||||||
|
func dockerEntrypoint(ctx context.Context, step actionStep, eval ExpressionEvaluator, stage stepStage) ([]string, error) {
|
||||||
|
runs := step.getActionModel().Runs
|
||||||
|
|
||||||
|
var entrypoint string
|
||||||
|
switch stage {
|
||||||
|
case stepStagePre:
|
||||||
|
entrypoint = runs.PreEntrypoint
|
||||||
|
case stepStagePost:
|
||||||
|
entrypoint = runs.PostEntrypoint
|
||||||
|
default:
|
||||||
|
if fields := strings.Fields(eval.Interpolate(ctx, step.getStepModel().With["entrypoint"])); len(fields) > 0 {
|
||||||
|
return fields, nil
|
||||||
|
}
|
||||||
|
entrypoint = runs.Entrypoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if entrypoint == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return shellquote.Split(entrypoint)
|
||||||
|
}
|
||||||
|
|
||||||
func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[]string) {
|
func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[]string) {
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
stepModel := step.getStepModel()
|
||||||
@@ -559,44 +576,57 @@ func hasPreStep(step actionStep) common.Conditional {
|
|||||||
return action.Runs.Using.IsComposite() ||
|
return action.Runs.Using.IsComposite() ||
|
||||||
(action.Runs.Using.IsNode() &&
|
(action.Runs.Using.IsNode() &&
|
||||||
action.Runs.Pre != "") ||
|
action.Runs.Pre != "") ||
|
||||||
|
(action.Runs.Using.IsDocker() &&
|
||||||
|
action.Runs.PreEntrypoint != "") ||
|
||||||
(action.Runs.Using == model.ActionRunsUsingGo &&
|
(action.Runs.Using == model.ActionRunsUsingGo &&
|
||||||
action.Runs.Pre != "")
|
action.Runs.Pre != "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// actionStagePaths resolves where a step's action lives and where the job container sees
|
||||||
|
// it, for the pre and post stage.
|
||||||
|
func actionStagePaths(step actionStep) (actionDir, actionPath, actionName, containerActionDir string) {
|
||||||
|
rc := step.getRunContext()
|
||||||
|
stepModel := step.getStepModel()
|
||||||
|
|
||||||
|
if _, ok := step.(*stepActionRemote); ok {
|
||||||
|
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), stepModel.UsesHash())
|
||||||
|
actionPath = newRemoteAction(stepModel.Uses).Path
|
||||||
|
} else {
|
||||||
|
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionName, containerActionDir = getContainerActionPaths(stepModel, path.Join(actionDir, actionPath), rc)
|
||||||
|
return actionDir, actionPath, actionName, containerActionDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// execDockerActionStage runs a docker action's image for its pre or post stage.
|
||||||
|
func execDockerActionStage(ctx context.Context, step actionStep, stage stepStage) error {
|
||||||
|
actionDir, actionPath, actionName, containerActionDir := actionStagePaths(step)
|
||||||
|
|
||||||
|
_, remote := step.(*stepActionRemote)
|
||||||
|
location := containerActionDir
|
||||||
|
if remote {
|
||||||
|
location = path.Join(actionDir, actionPath)
|
||||||
|
}
|
||||||
|
return execAsDocker(ctx, step, actionName, actionDir, location, !remote, stage)
|
||||||
|
}
|
||||||
|
|
||||||
func runPreStep(step actionStep) common.Executor {
|
func runPreStep(step actionStep) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Debugf("run pre step for '%s'", step.getStepModel())
|
logger.Debugf("run pre step for '%s'", step.getStepModel())
|
||||||
|
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
|
|
||||||
|
actionDir, actionPath, _, containerActionDir := actionStagePaths(step)
|
||||||
|
|
||||||
x := action.Runs.Using
|
x := action.Runs.Using
|
||||||
switch {
|
switch {
|
||||||
case x.IsNode():
|
case x.IsNode():
|
||||||
// defaults in pre steps were missing, however provided inputs are available
|
// defaults in pre steps were missing, however provided inputs are available
|
||||||
populateEnvsFromInput(ctx, step.getEnv(), action, rc)
|
populateEnvsFromInput(ctx, step.getEnv(), action, rc)
|
||||||
// todo: refactor into step
|
|
||||||
var actionDir string
|
|
||||||
var actionPath string
|
|
||||||
if _, ok := step.(*stepActionRemote); ok {
|
|
||||||
actionPath = newRemoteAction(stepModel.Uses).Path
|
|
||||||
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), stepModel.UsesHash())
|
|
||||||
} else {
|
|
||||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
|
||||||
actionPath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionLocation string
|
|
||||||
if actionPath != "" {
|
|
||||||
actionLocation = path.Join(actionDir, actionPath)
|
|
||||||
} else {
|
|
||||||
actionLocation = actionDir
|
|
||||||
}
|
|
||||||
|
|
||||||
_, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
|
|
||||||
|
|
||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -609,6 +639,12 @@ func runPreStep(step actionStep) common.Executor {
|
|||||||
|
|
||||||
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
||||||
|
|
||||||
|
case x.IsDocker():
|
||||||
|
// defaults in pre steps were missing, however provided inputs are available
|
||||||
|
populateEnvsFromInput(ctx, step.getEnv(), action, rc)
|
||||||
|
|
||||||
|
return execDockerActionStage(ctx, step, stepStagePre)
|
||||||
|
|
||||||
case x.IsComposite():
|
case x.IsComposite():
|
||||||
if step.getCompositeSteps() == nil {
|
if step.getCompositeSteps() == nil {
|
||||||
step.getCompositeRunContext(ctx)
|
step.getCompositeRunContext(ctx)
|
||||||
@@ -622,25 +658,6 @@ func runPreStep(step actionStep) common.Executor {
|
|||||||
case x == model.ActionRunsUsingGo:
|
case x == model.ActionRunsUsingGo:
|
||||||
// defaults in pre steps were missing, however provided inputs are available
|
// defaults in pre steps were missing, however provided inputs are available
|
||||||
populateEnvsFromInput(ctx, step.getEnv(), action, rc)
|
populateEnvsFromInput(ctx, step.getEnv(), action, rc)
|
||||||
// todo: refactor into step
|
|
||||||
var actionDir string
|
|
||||||
var actionPath string
|
|
||||||
if _, ok := step.(*stepActionRemote); ok {
|
|
||||||
actionPath = newRemoteAction(stepModel.Uses).Path
|
|
||||||
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), stepModel.UsesHash())
|
|
||||||
} else {
|
|
||||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
|
||||||
actionPath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionLocation string
|
|
||||||
if actionPath != "" {
|
|
||||||
actionLocation = path.Join(actionDir, actionPath)
|
|
||||||
} else {
|
|
||||||
actionLocation = actionDir
|
|
||||||
}
|
|
||||||
|
|
||||||
_, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
|
|
||||||
|
|
||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -693,6 +710,8 @@ func hasPostStep(step actionStep) common.Conditional {
|
|||||||
return action.Runs.Using.IsComposite() ||
|
return action.Runs.Using.IsComposite() ||
|
||||||
(action.Runs.Using.IsNode() &&
|
(action.Runs.Using.IsNode() &&
|
||||||
action.Runs.Post != "") ||
|
action.Runs.Post != "") ||
|
||||||
|
(action.Runs.Using.IsDocker() &&
|
||||||
|
action.Runs.PostEntrypoint != "") ||
|
||||||
(action.Runs.Using == model.ActionRunsUsingGo &&
|
(action.Runs.Using == model.ActionRunsUsingGo &&
|
||||||
action.Runs.Post != "")
|
action.Runs.Post != "")
|
||||||
}
|
}
|
||||||
@@ -704,28 +723,9 @@ func runPostStep(step actionStep) common.Executor {
|
|||||||
logger.Debugf("run post step for '%s'", step.getStepModel())
|
logger.Debugf("run post step for '%s'", step.getStepModel())
|
||||||
|
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
|
|
||||||
// todo: refactor into step
|
actionDir, actionPath, _, containerActionDir := actionStagePaths(step)
|
||||||
var actionDir string
|
|
||||||
var actionPath string
|
|
||||||
if _, ok := step.(*stepActionRemote); ok {
|
|
||||||
actionPath = newRemoteAction(stepModel.Uses).Path
|
|
||||||
actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), stepModel.UsesHash())
|
|
||||||
} else {
|
|
||||||
actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
|
|
||||||
actionPath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionLocation string
|
|
||||||
if actionPath != "" {
|
|
||||||
actionLocation = path.Join(actionDir, actionPath)
|
|
||||||
} else {
|
|
||||||
actionLocation = actionDir
|
|
||||||
}
|
|
||||||
|
|
||||||
_, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
|
|
||||||
|
|
||||||
x := action.Runs.Using
|
x := action.Runs.Using
|
||||||
switch {
|
switch {
|
||||||
@@ -740,6 +740,11 @@ func runPostStep(step actionStep) common.Executor {
|
|||||||
|
|
||||||
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
|
||||||
|
|
||||||
|
case x.IsDocker():
|
||||||
|
populateEnvsFromSavedState(step.getEnv(), step, rc)
|
||||||
|
|
||||||
|
return execDockerActionStage(ctx, step, stepStagePost)
|
||||||
|
|
||||||
case x.IsComposite():
|
case x.IsComposite():
|
||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func TestExecAsDockerAutoRemove(t *testing.T) {
|
|||||||
cm.On("Start", true).Return(func(context.Context) error { return nil })
|
cm.On("Start", true).Return(func(context.Context) error { return nil })
|
||||||
cm.On("Close").Return(func(context.Context) error { return nil })
|
cm.On("Close").Return(func(context.Context) error { return nil })
|
||||||
|
|
||||||
require.NoError(t, execAsDocker(context.Background(), step, "action", t.TempDir(), t.TempDir(), false))
|
require.NoError(t, execAsDocker(context.Background(), step, "action", t.TempDir(), t.TempDir(), false, stepStageMain))
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
assert.Equal(t, tc.removes, removes)
|
assert.Equal(t, tc.removes, removes)
|
||||||
}
|
}
|
||||||
@@ -465,7 +465,7 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() { done <- execAsDocker(ctx, step, "test-action", actionDir, actionDir, false) }()
|
go func() { done <- execAsDocker(ctx, step, "test-action", actionDir, actionDir, false, stepStageMain) }()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-innerEntered:
|
case <-innerEntered:
|
||||||
@@ -541,3 +541,86 @@ func TestDockerActionImageTag(t *testing.T) {
|
|||||||
dockerActionImageTag("owner/repo", "./sub", true),
|
dockerActionImageTag("owner/repo", "./sub", true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only the entrypoint is stage specific: every stage of a docker action receives runs.args
|
||||||
|
// and runs.env, and the `entrypoint` input applies to the main stage alone.
|
||||||
|
func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||||
|
orig := ContainerNewContainer
|
||||||
|
defer func() { ContainerNewContainer = orig }()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
stage stepStage
|
||||||
|
wantEntrypoint []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "main stage prefers the entrypoint input",
|
||||||
|
stage: stepStageMain,
|
||||||
|
wantEntrypoint: []string{"input.sh"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pre stage uses runs.pre-entrypoint",
|
||||||
|
stage: stepStagePre,
|
||||||
|
wantEntrypoint: []string{"pre.sh", "--verbose"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "post stage uses runs.post-entrypoint",
|
||||||
|
stage: stepStagePost,
|
||||||
|
wantEntrypoint: []string{"post.sh"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cm := &containerMock{}
|
||||||
|
var input *container.NewContainerInput
|
||||||
|
ContainerNewContainer = func(in *container.NewContainerInput) container.ExecutionsEnvironment {
|
||||||
|
input = in
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
step := &stepActionRemote{
|
||||||
|
Step: &model.Step{ID: "1", Uses: "org/action@v1", With: map[string]string{"entrypoint": "input.sh"}},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
action: &model.Action{Runs: model.ActionRuns{
|
||||||
|
Using: "docker",
|
||||||
|
Image: "docker://node:14",
|
||||||
|
PreEntrypoint: "pre.sh --verbose",
|
||||||
|
Entrypoint: "main.sh",
|
||||||
|
PostEntrypoint: "post.sh",
|
||||||
|
Args: []string{"hello"},
|
||||||
|
Env: map[string]string{"MY_VAR": "world"},
|
||||||
|
}},
|
||||||
|
env: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.On("Pull", false).Return(func(context.Context) error { return nil })
|
||||||
|
cm.On("Remove").Return(func(context.Context) error { return nil })
|
||||||
|
cm.On("Create", []string(nil), []string(nil)).Return(func(context.Context) error { return nil })
|
||||||
|
cm.On("Start", true).Return(func(context.Context) error { return nil })
|
||||||
|
cm.On("Close").Return(func(context.Context) error { return nil })
|
||||||
|
|
||||||
|
require.NoError(t, execAsDocker(context.Background(), step, "action", t.TempDir(), t.TempDir(), false, tc.stage))
|
||||||
|
require.NotNil(t, input)
|
||||||
|
assert.Equal(t, tc.wantEntrypoint, input.Entrypoint)
|
||||||
|
assert.Equal(t, []string{"hello"}, input.Cmd)
|
||||||
|
assert.Contains(t, input.Env, "MY_VAR=world")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDockerActionHasPreAndPostStep(t *testing.T) {
|
||||||
|
newStep := func(runs model.ActionRuns) actionStep {
|
||||||
|
return &stepActionRemote{action: &model.Action{Runs: runs}}
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.False(t, hasPreStep(newStep(model.ActionRuns{Using: "docker", Image: "Dockerfile"}))(ctx))
|
||||||
|
assert.False(t, hasPostStep(newStep(model.ActionRuns{Using: "docker", Image: "Dockerfile"}))(ctx))
|
||||||
|
|
||||||
|
withStages := model.ActionRuns{Using: "docker", Image: "Dockerfile", PreEntrypoint: "pre.sh", PostEntrypoint: "post.sh"}
|
||||||
|
assert.True(t, hasPreStep(newStep(withStages))(ctx))
|
||||||
|
assert.True(t, hasPostStep(newStep(withStages))(ctx))
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,9 +95,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
|||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
HashFiles: getHashFilesFunction(ctx, rc),
|
HashFiles: getHashFilesFunction(ctx, rc),
|
||||||
}
|
}
|
||||||
if rc.JobContainer != nil {
|
ee.Runner = rc.getRunnerContext(ctx)
|
||||||
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
|
|
||||||
}
|
|
||||||
return expressionEvaluator{
|
return expressionEvaluator{
|
||||||
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||||
Run: rc.Run,
|
Run: rc.Run,
|
||||||
@@ -149,9 +147,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
|
|||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
HashFiles: getHashFilesFunction(ctx, rc),
|
HashFiles: getHashFilesFunction(ctx, rc),
|
||||||
}
|
}
|
||||||
if rc.JobContainer != nil {
|
ee.Runner = rc.getRunnerContext(ctx)
|
||||||
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
|
|
||||||
}
|
|
||||||
return expressionEvaluator{
|
return expressionEvaluator{
|
||||||
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||||
Run: rc.Run,
|
Run: rc.Run,
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
AllocatePTY: rc.Config.AllocatePTY,
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
}
|
}
|
||||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
||||||
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
for k, v := range rc.getRunnerContext(ctx) {
|
||||||
if v, ok := v.(string); ok {
|
if v, ok := v.(string); ok {
|
||||||
rc.Env["RUNNER_"+strings.ToUpper(k)] = v
|
rc.Env["RUNNER_"+strings.ToUpper(k)] = v
|
||||||
}
|
}
|
||||||
@@ -986,6 +986,21 @@ func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
|
|||||||
return rc.StepResults
|
return rc.StepResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRunnerContext returns the `runner` context: what the execution environment knows
|
||||||
|
// (os, arch, temp, tool_cache) plus what only the runner process knows.
|
||||||
|
func (rc *RunContext) getRunnerContext(ctx context.Context) map[string]any {
|
||||||
|
runnerContext := map[string]any{}
|
||||||
|
if rc.JobContainer != nil {
|
||||||
|
maps0.Copy(runnerContext, rc.JobContainer.GetRunnerContext(ctx))
|
||||||
|
}
|
||||||
|
runnerContext["name"] = rc.Config.RunnerName
|
||||||
|
runnerContext["environment"] = "self-hosted"
|
||||||
|
if rc.Config.RunnerDebug() {
|
||||||
|
runnerContext["debug"] = "1"
|
||||||
|
}
|
||||||
|
return runnerContext
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
|
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
ghc := &model.GithubContext{
|
ghc := &model.GithubContext{
|
||||||
@@ -1170,7 +1185,7 @@ func nestedMapLookup(m map[string]any, ks ...string) (rval any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string { //nolint:unparam // pre-existing issue from nektos/act
|
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
|
||||||
env["GITHUB_RUN_ID"] = github.RunID
|
env["GITHUB_RUN_ID"] = github.RunID
|
||||||
@@ -1212,23 +1227,71 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
|
|||||||
env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt
|
env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env["RUNNER_NAME"] = rc.Config.RunnerName
|
||||||
|
env["RUNNER_ENVIRONMENT"] = "self-hosted"
|
||||||
|
if workspace := parentDir(github.Workspace); workspace != "" {
|
||||||
|
env["RUNNER_WORKSPACE"] = workspace
|
||||||
|
}
|
||||||
|
if rc.Config.RunnerDebug() {
|
||||||
|
env["RUNNER_DEBUG"] = "1"
|
||||||
|
}
|
||||||
|
|
||||||
if rc.Config.ArtifactServerPath != "" {
|
if rc.Config.ArtifactServerPath != "" {
|
||||||
setActionRuntimeVars(rc, env)
|
setActionRuntimeVars(rc, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, platformName := range rc.runsOnPlatformNames(ctx) {
|
if imageOS := rc.imageOS(ctx); imageOS != "" {
|
||||||
if platformName != "" {
|
env["ImageOS"] = imageOS
|
||||||
if platformName == "ubuntu-latest" {
|
|
||||||
// hardcode current ubuntu-latest since we have no way to check that 'on the fly'
|
|
||||||
env["ImageOS"] = "ubuntu20"
|
|
||||||
} else {
|
|
||||||
platformName = strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0]
|
|
||||||
env["ImageOS"] = platformName
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentDir returns the directory containing p, or "" when p names no parent. Both
|
||||||
|
// separators are accepted rather than filepath's, as p may describe a container while
|
||||||
|
// the runner itself runs on Windows, or the other way round.
|
||||||
|
func parentDir(p string) string {
|
||||||
|
if slash := strings.LastIndexAny(p, `/\`); slash > 0 {
|
||||||
|
return p[:slash]
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageOS returns ImageOS, which setup-* actions use to tell one runner image release
|
||||||
|
// from another. The resolved image tag is preferred over the runs-on label because it
|
||||||
|
// still names a release when the label is a rolling one such as ubuntu-latest.
|
||||||
|
func (rc *RunContext) imageOS(ctx context.Context) string {
|
||||||
|
if rc.Run.Job().RunsOn() == nil {
|
||||||
|
// A composite action runs on a synthetic job, and resolving its image would only
|
||||||
|
// log that runs-on is missing.
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if imageOS := imageOSFromImage(rc.platformImage(ctx)); imageOS != "" {
|
||||||
|
return imageOS
|
||||||
}
|
}
|
||||||
|
|
||||||
return env
|
for _, platformName := range slices.Backward(rc.runsOnPlatformNames(ctx)) {
|
||||||
|
if platformName == "ubuntu-latest" {
|
||||||
|
// Rolling label whose image names no release either, so keep the historical value.
|
||||||
|
return "ubuntu20"
|
||||||
|
} else if platformName != "" {
|
||||||
|
return strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageOSTag matches an image reference tagged with an OS family ImageOS can report plus
|
||||||
|
// its release, such as "docker.gitea.com/runner-images:ubuntu-24.04". Anything else
|
||||||
|
// ("ubuntu-latest", "app:22.04", "catthehacker/ubuntu:act-22.04", or a registry port) is
|
||||||
|
// left to the runs-on label rather than turned into a bogus OS.
|
||||||
|
var imageOSTag = regexp.MustCompile(`:(ubuntu|win|macos)-?([0-9]+)[^/]*$`)
|
||||||
|
|
||||||
|
// imageOSFromImage derives ImageOS from an image reference, e.g.
|
||||||
|
// "docker.gitea.com/runner-images:ubuntu-24.04" yields "ubuntu24".
|
||||||
|
func imageOSFromImage(image string) string {
|
||||||
|
if match := imageOSTag.FindStringSubmatch(image); match != nil {
|
||||||
|
return match[1] + match[2]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
||||||
|
|||||||
@@ -940,3 +940,117 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) })
|
assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageOSFromImage(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
image string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"docker.gitea.com/runner-images:ubuntu-24.04", "ubuntu24"},
|
||||||
|
{"docker.gitea.com/runner-images:ubuntu-latest", ""},
|
||||||
|
{"runner-images:ubuntu22.04", "ubuntu22"},
|
||||||
|
{"node:20", ""},
|
||||||
|
{"ubuntu:22.04", ""},
|
||||||
|
{"ubuntu", ""},
|
||||||
|
{"catthehacker/ubuntu:act-22.04", ""},
|
||||||
|
{"myco/ubuntu:v2.1", ""},
|
||||||
|
{"myco/ubuntu:v22.04", ""},
|
||||||
|
{"app:release-1", ""},
|
||||||
|
{"app:1.2.3", ""},
|
||||||
|
{"app:build-2.1", ""},
|
||||||
|
{"registry.example.com:5000/runner-images", ""},
|
||||||
|
{"registry.example.com:5000/runner-images:ubuntu-24.04", "ubuntu24"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.image, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.want, imageOSFromImage(tc.image))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRunsOnRunContext(t *testing.T, runsOn string) *RunContext {
|
||||||
|
return createIfTestRunContext(map[string]*model.Job{
|
||||||
|
"job1": createJob(t, "runs-on: "+runsOn, ""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunContextImageOS(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("prefers the release in the resolved image tag", func(t *testing.T) {
|
||||||
|
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||||
|
rc.Config.Platforms = map[string]string{
|
||||||
|
"ubuntu-latest": "docker.gitea.com/runner-images:ubuntu-24.04",
|
||||||
|
}
|
||||||
|
assert.Equal(t, "ubuntu24", rc.imageOS(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls back to the runs-on label", func(t *testing.T) {
|
||||||
|
rc := createRunsOnRunContext(t, "ubuntu-22.04")
|
||||||
|
rc.Config.Platforms = map[string]string{"ubuntu-22.04": "some-image"}
|
||||||
|
assert.Equal(t, "ubuntu22", rc.imageOS(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps the historical value for a rolling label with no release", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "ubuntu20", createRunsOnRunContext(t, "ubuntu-latest").imageOS(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is empty for the synthetic job of a composite action", func(t *testing.T) {
|
||||||
|
rc := createIfTestRunContext(map[string]*model.Job{"job1": {}})
|
||||||
|
assert.Empty(t, rc.imageOS(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunContextGetRunnerContext(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("adds the runner values the container cannot know", func(t *testing.T) {
|
||||||
|
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||||
|
rc.Config.RunnerName = "runner-1"
|
||||||
|
|
||||||
|
runnerContext := rc.getRunnerContext(ctx)
|
||||||
|
assert.Equal(t, "runner-1", runnerContext["name"])
|
||||||
|
assert.Equal(t, "self-hosted", runnerContext["environment"])
|
||||||
|
assert.NotContains(t, runnerContext, "debug")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("reports debug when step debugging is on", func(t *testing.T) {
|
||||||
|
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||||
|
rc.Config.Secrets = map[string]string{"ACTIONS_STEP_DEBUG": "true"}
|
||||||
|
|
||||||
|
assert.Equal(t, "1", rc.getRunnerContext(ctx)["debug"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps the execution environment values", func(t *testing.T) {
|
||||||
|
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||||
|
rc.JobContainer = &container.HostEnvironment{TmpDir: "/tmp/act", ToolCache: "/tmp/tool_cache"}
|
||||||
|
|
||||||
|
runnerContext := rc.getRunnerContext(ctx)
|
||||||
|
assert.Equal(t, "/tmp/act", runnerContext["temp"])
|
||||||
|
assert.Equal(t, "/tmp/tool_cache", runnerContext["tool_cache"])
|
||||||
|
assert.NotEmpty(t, runnerContext["os"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParentDir(t *testing.T) {
|
||||||
|
assert.Empty(t, parentDir(""))
|
||||||
|
assert.Empty(t, parentDir("repo"))
|
||||||
|
assert.Empty(t, parentDir("/repo"))
|
||||||
|
assert.Equal(t, "/workspace/owner", parentDir("/workspace/owner/repo"))
|
||||||
|
assert.Equal(t, `C:\workspace\owner`, parentDir(`C:\workspace\owner\repo`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunContextWithGithubEnvRunnerValues(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||||
|
rc.Config.RunnerName = "runner-1"
|
||||||
|
rc.Config.Secrets = map[string]string{"ACTIONS_STEP_DEBUG": "true"}
|
||||||
|
|
||||||
|
env := map[string]string{}
|
||||||
|
rc.withGithubEnv(ctx, &model.GithubContext{Workspace: "/workspace/owner/repo"}, env)
|
||||||
|
|
||||||
|
assert.Equal(t, "runner-1", env["RUNNER_NAME"])
|
||||||
|
assert.Equal(t, "self-hosted", env["RUNNER_ENVIRONMENT"])
|
||||||
|
assert.Equal(t, "/workspace/owner", env["RUNNER_WORKSPACE"])
|
||||||
|
assert.Equal(t, "1", env["RUNNER_DEBUG"])
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,6 +92,14 @@ type Config struct {
|
|||||||
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
||||||
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
||||||
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||||
|
RunnerName string // name this runner registered with, reported as `runner.name`, defaults to the hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerDebug reports whether debug logging is on, exposed as `runner.debug` and
|
||||||
|
// RUNNER_DEBUG. Only the secret also makes the reporter keep ::debug:: output, the env
|
||||||
|
// is accepted for `exec` and for runners configured with it.
|
||||||
|
func (c Config) RunnerDebug() bool {
|
||||||
|
return c.Secrets["ACTIONS_STEP_DEBUG"] == "true" || c.Env["ACTIONS_STEP_DEBUG"] == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToken: Adapt to Gitea
|
// GetToken: Adapt to Gitea
|
||||||
@@ -137,6 +145,11 @@ func New(runnerConfig *Config) (Runner, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (runner *runnerImpl) configure() (Runner, error) {
|
func (runner *runnerImpl) configure() (Runner, error) {
|
||||||
|
if runner.config.RunnerName == "" {
|
||||||
|
// Callers that do not register, such as `exec`, still get a `runner.name`.
|
||||||
|
runner.config.RunnerName, _ = os.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
runner.eventJSON = "{}"
|
runner.eventJSON = "{}"
|
||||||
if runner.config.EventJSON != "" {
|
if runner.config.EventJSON != "" {
|
||||||
runner.eventJSON = runner.config.EventJSON
|
runner.eventJSON = runner.config.EventJSON
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
delete((env), "GITHUB_REPOSITORY")
|
delete((env), "GITHUB_REPOSITORY")
|
||||||
delete((env), "GITHUB_REPOSITORY_OWNER")
|
delete((env), "GITHUB_REPOSITORY_OWNER")
|
||||||
delete((env), "GITHUB_ACTOR")
|
delete((env), "GITHUB_ACTOR")
|
||||||
|
// Host-dependent, asserted in TestRunContextWithGithubEnvRunnerValues instead.
|
||||||
|
delete((env), "RUNNER_NAME")
|
||||||
|
delete((env), "RUNNER_WORKSPACE")
|
||||||
|
|
||||||
assert.Equal(t, map[string]string{
|
assert.Equal(t, map[string]string{
|
||||||
"ACT": "true",
|
"ACT": "true",
|
||||||
@@ -192,6 +195,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
"GITHUB_WORKFLOW": "",
|
"GITHUB_WORKFLOW": "",
|
||||||
"INPUT_STEP_WITH": "with-value",
|
"INPUT_STEP_WITH": "with-value",
|
||||||
"RC_KEY": "rcvalue",
|
"RC_KEY": "rcvalue",
|
||||||
|
"RUNNER_ENVIRONMENT": "self-hosted",
|
||||||
"RUNNER_PERFLOG": "/dev/null",
|
"RUNNER_PERFLOG": "/dev/null",
|
||||||
"RUNNER_TRACKING_ID": "",
|
"RUNNER_TRACKING_ID": "",
|
||||||
}, env)
|
}, env)
|
||||||
|
|||||||
@@ -474,6 +474,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
Vars: task.Vars,
|
Vars: task.Vars,
|
||||||
ValidVolumes: r.cfg.Container.ValidVolumes,
|
ValidVolumes: r.cfg.Container.ValidVolumes,
|
||||||
InsecureSkipTLS: r.cfg.Runner.Insecure,
|
InsecureSkipTLS: r.cfg.Runner.Insecure,
|
||||||
|
RunnerName: r.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
rr, err := runner.New(runnerConfig)
|
rr, err := runner.New(runnerConfig)
|
||||||
|
|||||||
Reference in New Issue
Block a user