From 0c4374ec419b3b7fbbe82d2d79677508085d90e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Brauer?= Date: Wed, 5 May 2021 18:42:34 +0200 Subject: [PATCH] GitHub Enterprise support (#658) * Add option to specify custom GitHub instance * Use correct GHE API endpoint URLs Co-authored-by: Markus Wolf * Extract slug from GitHub Enterprise URLs Co-authored-by: Markus Wolf * Use GITHUB_TOKEN for clone authenticate if provided This change will allow use authentication for cloning actions from private repositories or github enterprise instances. Co-Authored-By: Markus Wolf * Add section about using act on GitHub Enterprise to README Co-authored-by: Markus Wolf * Set GitHubInstance in runnerConfig in runner_test Co-authored-by: Markus Wolf Co-authored-by: hackercat Co-authored-by: Markus Wolf --- README.md | 10 +++++++++ cmd/input.go | 1 + cmd/root.go | 2 ++ pkg/common/git.go | 44 +++++++++++++++++++++++++++++--------- pkg/common/git_test.go | 2 +- pkg/runner/run_context.go | 7 +++++- pkg/runner/runner.go | 1 + pkg/runner/runner_test.go | 1 + pkg/runner/step_context.go | 17 ++++++++++----- 9 files changed, 68 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 757fe0a..60dde45 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co --env stringArray env to make available to actions with optional value (e.g. --e myenv=foo or -s 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. @@ -306,6 +307,15 @@ act -e pull-request.json Act will properly provide `github.head_ref` and `github.base_ref` to the action as expected. +# GitHub Enterprise + +Act supports using and authenticating against private GitHub Enterprise servers. +To use your custom GHE server, set the CLI flag `--github-instance` to your hostname (e.g. `github.company.com`). + +Please note that if your GHE server requires authentication, we will use the secret provided via `GITHUB_TOKEN`. + +Please also see the [official documentation for GitHub actions on GHE](https://docs.github.com/en/enterprise-server@3.0/admin/github-actions/about-using-actions-in-your-enterprise) for more information on how to use actions. + # Support Need help? Ask on [Gitter](https://gitter.im/nektos/act)! diff --git a/cmd/input.go b/cmd/input.go index f9f36b4..e954287 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -29,6 +29,7 @@ type Input struct { containerArchitecture string noWorkflowRecurse bool useGitIgnore bool + githubInstance string } func (i *Input) resolve(path string) string { diff --git a/cmd/root.go b/cmd/root.go index 260aa34..13867b9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,6 +61,7 @@ func Execute(ctx context.Context, version string) { rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.") rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers") rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "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.") + rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.") rootCmd.SetArgs(args()) if err := rootCmd.Execute(); err != nil { @@ -254,6 +255,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str UsernsMode: input.usernsMode, ContainerArchitecture: input.containerArchitecture, UseGitIgnore: input.useGitIgnore, + GitHubInstance: input.githubInstance, } r, err := runner.New(config) if err != nil { diff --git a/pkg/common/git.go b/pkg/common/git.go index 2422091..db98f81 100644 --- a/pkg/common/git.go +++ b/pkg/common/git.go @@ -15,6 +15,7 @@ import ( git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-ini/ini" log "github.com/sirupsen/logrus" ) @@ -142,12 +143,12 @@ func findGitPrettyRef(head, root, sub string) (string, error) { } // FindGithubRepo get the repo -func FindGithubRepo(file string) (string, error) { +func FindGithubRepo(file string, githubInstance string) (string, error) { url, err := findGitRemoteURL(file) if err != nil { return "", err } - _, slug, err := findGitSlug(url) + _, slug, err := findGitSlug(url, githubInstance) return slug, err } @@ -174,7 +175,7 @@ func findGitRemoteURL(file string) (string, error) { return url, nil } -func findGitSlug(url string) (string, string, error) { +func findGitSlug(url string, githubInstance string) (string, string, error) { if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil { return "CodeCommit", matches[2], nil } else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil { @@ -183,6 +184,14 @@ func findGitSlug(url string) (string, string, error) { return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil } else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil { return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil + } else if githubInstance != "github.com" { + gheHTTPRegex := regexp.MustCompile(fmt.Sprintf(`^https?://%s/(.+)/(.+?)(?:.git)?$`, githubInstance)) + gheSSHRegex := regexp.MustCompile(fmt.Sprintf(`%s[:/](.+)/(.+).git$`, githubInstance)) + if matches := gheHTTPRegex.FindStringSubmatch(url); matches != nil { + return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil + } else if matches := gheSSHRegex.FindStringSubmatch(url); matches != nil { + return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil + } } return "", url, nil } @@ -218,9 +227,10 @@ func findGitDirectory(fromFile string) (string, error) { // NewGitCloneExecutorInput the input for the NewGitCloneExecutor type NewGitCloneExecutorInput struct { - URL string - Ref string - Dir string + URL string + Ref string + Dir string + Token string } // CloneIfRequired ... @@ -237,10 +247,24 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input progressWriter = os.Stdout } - r, err = git.PlainCloneContext(ctx, input.Dir, false, &git.CloneOptions{ - URL: input.URL, - Progress: progressWriter, - }) + var cloneOptions git.CloneOptions + if input.Token != "" { + cloneOptions = git.CloneOptions{ + URL: input.URL, + Progress: progressWriter, + Auth: &http.BasicAuth{ + Username: "token", + Password: input.Token, + }, + } + } else { + cloneOptions = git.CloneOptions{ + URL: input.URL, + Progress: progressWriter, + } + } + + r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions) if err != nil { logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err) return nil, err diff --git a/pkg/common/git_test.go b/pkg/common/git_test.go index 1567c24..9c3bf1e 100644 --- a/pkg/common/git_test.go +++ b/pkg/common/git_test.go @@ -34,7 +34,7 @@ func TestFindGitSlug(t *testing.T) { } for _, tt := range slugTests { - provider, slug, err := findGitSlug(tt.url) + provider, slug, err := findGitSlug(tt.url, "github.com") assert.NoError(err) assert.Equal(tt.provider, provider) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index e3d9648..1c4bba1 100755 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -503,7 +503,7 @@ func (rc *RunContext) getGithubContext() *githubContext { } repoPath := rc.Config.Workdir - repo, err := common.FindGithubRepo(repoPath) + repo, err := common.FindGithubRepo(repoPath, rc.Config.GitHubInstance) if err != nil { log.Warningf("unable to get git repo: %v", err) } else { @@ -644,6 +644,11 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string { env["GITHUB_SERVER_URL"] = "https://github.com" env["GITHUB_API_URL"] = "https://api.github.com" env["GITHUB_GRAPHQL_URL"] = "https://api.github.com/graphql" + if rc.Config.GitHubInstance != "github.com" { + env["GITHUB_SERVER_URL"] = fmt.Sprintf("https://%s", rc.Config.GitHubInstance) + env["GITHUB_API_URL"] = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance) + env["GITHUB_GRAPHQL_URL"] = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance) + } job := rc.Run.Job() if job.RunsOn() != nil { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index ff28cf6..9262b96 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -38,6 +38,7 @@ type Config struct { UsernsMode string // user namespace to use ContainerArchitecture string // Desired OS/architecture platform for running containers UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true + GitHubInstance string // GitHub instance to use, default "github.com" } // Resolves the equivalent host path inside the container diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 044625e..ddf99b6 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -56,6 +56,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo, sec ReuseContainers: false, ContainerArchitecture: tjfi.containerArchitecture, Secrets: secrets, + GitHubInstance: "github.com", } runner, err := New(runnerConfig) diff --git a/pkg/runner/step_context.go b/pkg/runner/step_context.go index fad38e4..63a5c28 100755 --- a/pkg/runner/step_context.go +++ b/pkg/runner/step_context.go @@ -70,7 +70,11 @@ func (sc *StepContext) Executor() common.Executor { if remoteAction == nil { return common.NewErrorExecutor(formatError(step.Uses)) } - if remoteAction.IsCheckout() && rc.getGithubContext().isLocalCheckout(step) { + + remoteAction.URL = rc.Config.GitHubInstance + + github := rc.getGithubContext() + if remoteAction.IsCheckout() && github.isLocalCheckout(step) { return func(ctx context.Context) error { common.Logger(ctx).Debugf("Skipping actions/checkout") return nil @@ -80,9 +84,10 @@ func (sc *StepContext) Executor() common.Executor { actionDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(step.Uses, "/", "-")) return common.NewPipelineExecutor( common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{ - URL: remoteAction.CloneURL(), - Ref: remoteAction.Ref, - Dir: actionDir, + URL: remoteAction.CloneURL(), + Ref: remoteAction.Ref, + Dir: actionDir, + Token: github.Token, }), sc.setupAction(actionDir, remoteAction.Path), sc.runAction(actionDir, remoteAction.Path), @@ -568,6 +573,7 @@ func (sc *StepContext) runAction(actionDir string, actionPath string) common.Exe } type remoteAction struct { + URL string Org string Repo string Path string @@ -575,7 +581,7 @@ type remoteAction struct { } func (ra *remoteAction) CloneURL() string { - return fmt.Sprintf("https://github.com/%s/%s", ra.Org, ra.Repo) + return fmt.Sprintf("https://%s/%s/%s", ra.URL, ra.Org, ra.Repo) } func (ra *remoteAction) IsCheckout() bool { @@ -601,6 +607,7 @@ func newRemoteAction(action string) *remoteAction { Repo: matches[2], Path: matches[4], Ref: matches[6], + URL: "github.com", } }