mirror of
https://gitea.com/gitea/runner.git
synced 2026-09-01 17:07:46 +00:00
fix: preserve symlinked Node action entrypoints (#1202)
Node resolves an ESM main to its realpath but leaves `process.argv[1]` as passed. `/var/run/act` reaches the action through the `/var/run` -> `/run` symlink most images ship, so the two disagree and actions comparing them skip their own `run()`. Passing `--preserve-symlinks-main` makes them match. Trade-off: an action whose `runs.main` is a symlink now resolves dependencies from the link's directory rather than the target's. Fixes https://gitea.com/gitea/runner/issues/1201 Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1202 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: bircni <bircni@noreply.gitea.com>
This commit is contained in:
@@ -185,7 +185,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
|||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Main)}
|
containerArgs := nodeActionCommand(path.Join(containerActionDir, action.Runs.Main))
|
||||||
logger.Debugf("executing remote job container: %s", containerArgs)
|
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||||
|
|
||||||
rc.ApplyExtraPath(ctx, step.getEnv())
|
rc.ApplyExtraPath(ctx, step.getEnv())
|
||||||
@@ -232,6 +232,11 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /var/run is a symlink, so without the flag node's import.meta.url differs from argv[1], which ESM actions compare.
|
||||||
|
func nodeActionCommand(script string) []string {
|
||||||
|
return []string{"node", "--preserve-symlinks-main", script}
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/nektos/act/issues/228#issuecomment-629709055
|
// https://github.com/nektos/act/issues/228#issuecomment-629709055
|
||||||
// files in .gitignore are not copied in a Docker container
|
// files in .gitignore are not copied in a Docker container
|
||||||
// this causes issues with actions that ignore other important resources
|
// this causes issues with actions that ignore other important resources
|
||||||
@@ -592,7 +597,7 @@ func runPreStep(step actionStep) common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Pre)}
|
containerArgs := nodeActionCommand(path.Join(containerActionDir, action.Runs.Pre))
|
||||||
logger.Debugf("executing remote job container: %s", containerArgs)
|
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||||
|
|
||||||
rc.ApplyExtraPath(ctx, step.getEnv())
|
rc.ApplyExtraPath(ctx, step.getEnv())
|
||||||
@@ -693,7 +698,7 @@ func runPostStep(step actionStep) common.Executor {
|
|||||||
|
|
||||||
populateEnvsFromSavedState(step.getEnv(), step, rc)
|
populateEnvsFromSavedState(step.getEnv(), step, rc)
|
||||||
|
|
||||||
containerArgs := []string{"node", path.Join(containerActionDir, action.Runs.Post)}
|
containerArgs := nodeActionCommand(path.Join(containerActionDir, action.Runs.Post))
|
||||||
logger.Debugf("executing remote job container: %s", containerArgs)
|
logger.Debugf("executing remote job container: %s", containerArgs)
|
||||||
|
|
||||||
rc.ApplyExtraPath(ctx, step.getEnv())
|
rc.ApplyExtraPath(ctx, step.getEnv())
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -234,7 +238,7 @@ func TestActionRunner(t *testing.T) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
cm.On("Exec", []string{"node", "/var/run/act/actions/dir/path"}, envMatcher, "", "").Return(func(ctx context.Context) error { return nil })
|
cm.On("Exec", []string{"node", "--preserve-symlinks-main", "/var/run/act/actions/dir/path"}, envMatcher, "", "").Return(func(ctx context.Context) error { return nil })
|
||||||
|
|
||||||
tt.step.getRunContext().JobContainer = cm
|
tt.step.getRunContext().JobContainer = cm
|
||||||
|
|
||||||
@@ -246,6 +250,32 @@ func TestActionRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNodeActionCommandPreservesSymlinkedEntrypoint(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("symlink creation requires privileges on Windows")
|
||||||
|
}
|
||||||
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
|
actionDir := t.TempDir()
|
||||||
|
entrypoint := filepath.Join(actionDir, "index.mjs")
|
||||||
|
require.NoError(t, os.WriteFile(entrypoint, []byte(`
|
||||||
|
import {fileURLToPath} from "node:url";
|
||||||
|
const self = fileURLToPath(import.meta.url);
|
||||||
|
if (process.argv[1] !== self) {
|
||||||
|
console.log("argv[1]:", process.argv[1], "import.meta.url:", self);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
`), 0o600))
|
||||||
|
|
||||||
|
symlinkedActionDir := filepath.Join(t.TempDir(), "action")
|
||||||
|
require.NoError(t, os.Symlink(actionDir, symlinkedActionDir))
|
||||||
|
symlinkedEntrypoint := filepath.Join(symlinkedActionDir, "index.mjs")
|
||||||
|
|
||||||
|
args := nodeActionCommand(symlinkedEntrypoint)
|
||||||
|
output, err := exec.CommandContext(t.Context(), args[0], args[1:]...).CombinedOutput()
|
||||||
|
require.NoError(t, err, "%s", output)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
|
func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,8 @@ func TestStepActionLocalPost(t *testing.T) {
|
|||||||
if tt.mocks.exec {
|
if tt.mocks.exec {
|
||||||
suffixMatcher := func(suffix string) any {
|
suffixMatcher := func(suffix string) any {
|
||||||
return mock.MatchedBy(func(array []string) bool {
|
return mock.MatchedBy(func(array []string) bool {
|
||||||
return strings.HasSuffix(array[1], suffix)
|
return len(array) == 3 && array[0] == "node" && array[1] == "--preserve-symlinks-main" &&
|
||||||
|
strings.HasSuffix(array[2], suffix)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
cm.On("Exec", suffixMatcher("runner/local/action/post.js"), sal.env, "", "").Return(func(ctx context.Context) error { return tt.err })
|
cm.On("Exec", suffixMatcher("runner/local/action/post.js"), sal.env, "", "").Return(func(ctx context.Context) error { return tt.err })
|
||||||
|
|||||||
@@ -470,10 +470,10 @@ func TestStepActionRemotePost(t *testing.T) {
|
|||||||
if tt.mocks.exec {
|
if tt.mocks.exec {
|
||||||
// Use mock.MatchedBy to match the exec command with hash-based path
|
// Use mock.MatchedBy to match the exec command with hash-based path
|
||||||
execMatcher := mock.MatchedBy(func(args []string) bool {
|
execMatcher := mock.MatchedBy(func(args []string) bool {
|
||||||
if len(args) != 2 {
|
if len(args) != 3 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return args[0] == "node" && strings.Contains(args[1], "post.js")
|
return args[0] == "node" && args[1] == "--preserve-symlinks-main" && strings.Contains(args[2], "post.js")
|
||||||
})
|
})
|
||||||
|
|
||||||
cm.On("Exec", execMatcher, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err })
|
cm.On("Exec", execMatcher, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err })
|
||||||
|
|||||||
Reference in New Issue
Block a user