From de37f750773575651a91363eb6b1ce97be964845 Mon Sep 17 00:00:00 2001 From: Neo Hsu Date: Tue, 21 Jun 2022 21:52:21 +0800 Subject: [PATCH] feat: add option to bypass GHE for actions checkout (#1162) * feat(#1161): add --through-action to assigned actions from GitHub * docs(flags): add --through-action and --through-action-token flags description * test(action, remote): add test case for ThroughAction * refactor(command): rename command from --through-action to --actions-from-github * refactor(command): rename command from --actions-from-github to --replace-ghe-action-with-github-com --- README.md | 74 +++++++------- cmd/input.go | 68 +++++++------ cmd/root.go | 62 +++++------ pkg/runner/runner.go | 60 +++++------ pkg/runner/step_action_remote.go | 8 ++ pkg/runner/step_action_remote_test.go | 141 ++++++++++++++++++++++++++ 6 files changed, 286 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index c0a8778..4186c06 100644 --- a/README.md +++ b/README.md @@ -159,42 +159,44 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co # Flags ```none - -a, --actor string user that triggered the event (default "nektos/act") - --artifact-server-path string Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start. - --artifact-server-port string Defines the port where the artifact server listens (will only bind to localhost). (default "34567") - -b, --bind bind working directory to container, rather than copy - --container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms. - --container-cap-add stringArray kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE) - --container-cap-drop stringArray kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE) - --container-daemon-socket string Path to Docker daemon socket which will be mounted to containers (default "/var/run/docker.sock") - --defaultbranch string the name of the main branch - --detect-event Use first event type from workflow as event that triggered the workflow - -C, --directory string working directory (default ".") - -n, --dryrun dryrun mode - --env stringArray env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv) - --env-file string environment file to read and use as env in the containers (default ".env") - -e, --eventpath string path to event JSON file - --github-instance string GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server. (default "github.com") - -g, --graph draw workflows - -h, --help help for act - --insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs. - -j, --job string run job - -l, --list list workflows - --no-recurse Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag - -P, --platform stringArray custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04) - --privileged use privileged mode - -p, --pull pull docker image(s) even if already present - -q, --quiet disable logging of output from steps - --rebuild rebuild local action docker image(s) even if already present - -r, --reuse don't remove container(s) on successfully completed workflow(s) to maintain state between runs - --rm automatically remove container(s)/volume(s) after a workflow(s) failure - -s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret) - --secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets") - --use-gitignore Controls whether paths specified in .gitignore should be copied into container (default true) - --userns string user namespace to use - -v, --verbose verbose output - -w, --watch watch the contents of the local repo and run when files change - -W, --workflows string path to workflow file(s) (default "./.github/workflows/") + -a, --actor string user that triggered the event (default "nektos/act") + --replace-ghe-action-with-github-com If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com=github/super-linter) + --replace-ghe-action-token-with-github-com If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set personal access token + --artifact-server-path string Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start. + --artifact-server-port string Defines the port where the artifact server listens (will only bind to localhost). (default "34567") + -b, --bind bind working directory to container, rather than copy + --container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms. + --container-cap-add stringArray kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE) + --container-cap-drop stringArray kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE) + --container-daemon-socket string Path to Docker daemon socket which will be mounted to containers (default "/var/run/docker.sock") + --defaultbranch string the name of the main branch + --detect-event Use first event type from workflow as event that triggered the workflow + -C, --directory string working directory (default ".") + -n, --dryrun dryrun mode + --env stringArray env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv) + --env-file string environment file to read and use as env in the containers (default ".env") + -e, --eventpath string path to event JSON file + --github-instance string GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server. (default "github.com") + -g, --graph draw workflows + -h, --help help for act + --insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs. + -j, --job string run job + -l, --list list workflows + --no-recurse Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag + -P, --platform stringArray custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04) + --privileged use privileged mode + -p, --pull pull docker image(s) even if already present + -q, --quiet disable logging of output from steps + --rebuild rebuild local action docker image(s) even if already present + -r, --reuse don't remove container(s) on successfully completed workflow(s) to maintain state between runs + --rm automatically remove container(s)/volume(s) after a workflow(s) failure + -s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret) + --secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets") + --use-gitignore Controls whether paths specified in .gitignore should be copied into container (default true) + --userns string user namespace to use + -v, --verbose verbose output + -w, --watch watch the contents of the local repo and run when files change + -W, --workflows string path to workflow file(s) (default "./.github/workflows/") ``` ## `GITHUB_TOKEN` diff --git a/cmd/input.go b/cmd/input.go index cd3397b..2de0fd2 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -8,39 +8,41 @@ import ( // Input contains the input for the root command type Input struct { - actor string - workdir string - workflowsPath string - autodetectEvent bool - eventPath string - reuseContainers bool - bindWorkdir bool - secrets []string - envs []string - platforms []string - dryrun bool - forcePull bool - forceRebuild bool - noOutput bool - envfile string - secretfile string - insecureSecrets bool - defaultBranch string - privileged bool - usernsMode string - containerArchitecture string - containerDaemonSocket string - noWorkflowRecurse bool - useGitIgnore bool - githubInstance string - containerCapAdd []string - containerCapDrop []string - autoRemove bool - artifactServerPath string - artifactServerPort string - jsonLogger bool - noSkipCheckout bool - remoteName string + actor string + workdir string + workflowsPath string + autodetectEvent bool + eventPath string + reuseContainers bool + bindWorkdir bool + secrets []string + envs []string + platforms []string + dryrun bool + forcePull bool + forceRebuild bool + noOutput bool + envfile string + secretfile string + insecureSecrets bool + defaultBranch string + privileged bool + usernsMode string + containerArchitecture string + containerDaemonSocket string + noWorkflowRecurse bool + useGitIgnore bool + githubInstance string + containerCapAdd []string + containerCapDrop []string + autoRemove bool + artifactServerPath string + artifactServerPort string + jsonLogger bool + noSkipCheckout bool + remoteName string + replaceGheActionWithGithubCom []string + replaceGheActionTokenWithGithubCom string } func (i *Input) resolve(path string) string { diff --git a/cmd/root.go b/cmd/root.go index d373da6..d2b2214 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,6 +61,8 @@ func Execute(ctx context.Context, version string) { rootCmd.Flags().StringArrayVarP(&input.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)") rootCmd.Flags().StringArrayVarP(&input.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)") rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure") + rootCmd.Flags().StringArrayVarP(&input.replaceGheActionWithGithubCom, "replace-ghe-action-with-github-com", "", []string{}, "If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com =github/super-linter)") + rootCmd.Flags().StringVar(&input.replaceGheActionTokenWithGithubCom, "replace-ghe-action-token-with-github-com", "", "If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set personal access token") rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event") rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)") rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag") @@ -370,35 +372,37 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str // run the plan config := &runner.Config{ - Actor: input.actor, - EventName: eventName, - EventPath: input.EventPath(), - DefaultBranch: defaultbranch, - ForcePull: input.forcePull, - ForceRebuild: input.forceRebuild, - ReuseContainers: input.reuseContainers, - Workdir: input.Workdir(), - BindWorkdir: input.bindWorkdir, - LogOutput: !input.noOutput, - JSONLogger: input.jsonLogger, - Env: envs, - Secrets: secrets, - Token: secrets["GITHUB_TOKEN"], - InsecureSecrets: input.insecureSecrets, - Platforms: input.newPlatforms(), - Privileged: input.privileged, - UsernsMode: input.usernsMode, - ContainerArchitecture: input.containerArchitecture, - ContainerDaemonSocket: input.containerDaemonSocket, - UseGitIgnore: input.useGitIgnore, - GitHubInstance: input.githubInstance, - ContainerCapAdd: input.containerCapAdd, - ContainerCapDrop: input.containerCapDrop, - AutoRemove: input.autoRemove, - ArtifactServerPath: input.artifactServerPath, - ArtifactServerPort: input.artifactServerPort, - NoSkipCheckout: input.noSkipCheckout, - RemoteName: input.remoteName, + Actor: input.actor, + EventName: eventName, + EventPath: input.EventPath(), + DefaultBranch: defaultbranch, + ForcePull: input.forcePull, + ForceRebuild: input.forceRebuild, + ReuseContainers: input.reuseContainers, + Workdir: input.Workdir(), + BindWorkdir: input.bindWorkdir, + LogOutput: !input.noOutput, + JSONLogger: input.jsonLogger, + Env: envs, + Secrets: secrets, + Token: secrets["GITHUB_TOKEN"], + InsecureSecrets: input.insecureSecrets, + Platforms: input.newPlatforms(), + Privileged: input.privileged, + UsernsMode: input.usernsMode, + ContainerArchitecture: input.containerArchitecture, + ContainerDaemonSocket: input.containerDaemonSocket, + UseGitIgnore: input.useGitIgnore, + GitHubInstance: input.githubInstance, + ContainerCapAdd: input.containerCapAdd, + ContainerCapDrop: input.containerCapDrop, + AutoRemove: input.autoRemove, + ArtifactServerPath: input.artifactServerPath, + ArtifactServerPort: input.artifactServerPort, + NoSkipCheckout: input.noSkipCheckout, + RemoteName: input.remoteName, + ReplaceGheActionWithGithubCom: input.replaceGheActionWithGithubCom, + ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom, } r, err := runner.New(config) if err != nil { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index e0bda07..64ba8e8 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -25,35 +25,37 @@ type Runner interface { // Config contains the config for a new runner type Config struct { - Actor string // the user that triggered the event - Workdir string // path to working directory - BindWorkdir bool // bind the workdir to the job container - EventName string // name of event to run - EventPath string // path to JSON file to use for event.json in containers - DefaultBranch string // name of the main branch for this repository - ReuseContainers bool // reuse containers to maintain state - ForcePull bool // force pulling of the image, even if already present - ForceRebuild bool // force rebuilding local docker image action - LogOutput bool // log the output from docker run - JSONLogger bool // use json or text logger - Env map[string]string // env for containers - Secrets map[string]string // list of secrets - Token string // GitHub token - InsecureSecrets bool // switch hiding output when printing to terminal - Platforms map[string]string // list of platforms - Privileged bool // use privileged mode - UsernsMode string // user namespace to use - ContainerArchitecture string // Desired OS/architecture platform for running containers - ContainerDaemonSocket string // Path to Docker daemon socket - UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true - GitHubInstance string // GitHub instance to use, default "github.com" - ContainerCapAdd []string // list of kernel capabilities to add to the containers - ContainerCapDrop []string // list of kernel capabilities to remove from the containers - AutoRemove bool // controls if the container is automatically removed upon workflow completion - ArtifactServerPath string // the path where the artifact server stores uploads - ArtifactServerPort string // the port the artifact server binds to - NoSkipCheckout bool // do not skip actions/checkout - RemoteName string // remote name in local git repo config + Actor string // the user that triggered the event + Workdir string // path to working directory + BindWorkdir bool // bind the workdir to the job container + EventName string // name of event to run + EventPath string // path to JSON file to use for event.json in containers + DefaultBranch string // name of the main branch for this repository + ReuseContainers bool // reuse containers to maintain state + ForcePull bool // force pulling of the image, even if already present + ForceRebuild bool // force rebuilding local docker image action + LogOutput bool // log the output from docker run + JSONLogger bool // use json or text logger + Env map[string]string // env for containers + Secrets map[string]string // list of secrets + Token string // GitHub token + InsecureSecrets bool // switch hiding output when printing to terminal + Platforms map[string]string // list of platforms + Privileged bool // use privileged mode + UsernsMode string // user namespace to use + ContainerArchitecture string // Desired OS/architecture platform for running containers + ContainerDaemonSocket string // Path to Docker daemon socket + UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true + GitHubInstance string // GitHub instance to use, default "github.com" + ContainerCapAdd []string // list of kernel capabilities to add to the containers + ContainerCapDrop []string // list of kernel capabilities to remove from the containers + AutoRemove bool // controls if the container is automatically removed upon workflow completion + ArtifactServerPath string // the path where the artifact server stores uploads + ArtifactServerPort string // the port the artifact server binds to + NoSkipCheckout bool // do not skip actions/checkout + RemoteName string // remote name in local git repo config + ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub + ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. } // Resolves the equivalent host path inside the container diff --git a/pkg/runner/step_action_remote.go b/pkg/runner/step_action_remote.go index 56b0ed5..aec46c9 100644 --- a/pkg/runner/step_action_remote.go +++ b/pkg/runner/step_action_remote.go @@ -55,6 +55,14 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor { return nil } + sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance + for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom { + if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) { + sar.remoteAction.URL = "github.com" + github.Token = sar.RunContext.Config.ReplaceGheActionTokenWithGithubCom + } + } + actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-")) gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{ URL: sar.remoteAction.CloneURL(), diff --git a/pkg/runner/step_action_remote_test.go b/pkg/runner/step_action_remote_test.go index 73b2331..1e117ea 100644 --- a/pkg/runner/step_action_remote_test.go +++ b/pkg/runner/step_action_remote_test.go @@ -256,6 +256,147 @@ func TestStepActionRemotePre(t *testing.T) { } } +func TestStepActionRemotePreThroughAction(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + }{ + { + name: "run-pre", + stepModel: &model.Step{ + Uses: "org/repo/path@ref", + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + clonedAction := false + sarm := &stepActionRemoteMocks{} + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + if input.URL == "https://github.com/org/repo" { + clonedAction = true + } + return nil + } + } + defer (func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + })() + + sar := &stepActionRemote{ + Step: tt.stepModel, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://enterprise.github.com", + ReplaceGheActionWithGithubCom: []string{"org/repo"}, + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + }, + readAction: sarm.readAction, + } + + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + + sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + + err := sar.pre()(ctx) + + assert.Nil(t, err) + assert.Equal(t, true, clonedAction) + + sarm.AssertExpectations(t) + }) + } +} + +func TestStepActionRemotePreThroughActionToken(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + }{ + { + name: "run-pre", + stepModel: &model.Step{ + Uses: "org/repo/path@ref", + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + clonedAction := false + sarm := &stepActionRemoteMocks{} + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + if input.URL == "https://github.com/org/repo" && input.Token == "PRIVATE_ACTIONS_TOKEN_ON_GITHUB" { + clonedAction = true + } + return nil + } + } + defer (func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + })() + + sar := &stepActionRemote{ + Step: tt.stepModel, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://enterprise.github.com", + ReplaceGheActionWithGithubCom: []string{"org/repo"}, + ReplaceGheActionTokenWithGithubCom: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB", + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + }, + readAction: sarm.readAction, + } + + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + + sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + + err := sar.pre()(ctx) + + assert.Nil(t, err) + assert.Equal(t, true, clonedAction) + + sarm.AssertExpectations(t) + }) + } +} + func TestStepActionRemotePost(t *testing.T) { table := []struct { name string