GitHub Enterprise support (#658)

* Add option to specify custom GitHub instance

* Use correct GHE API endpoint URLs

Co-authored-by: Markus Wolf <knister.peter@shadowrun-clan.de>

* Extract slug from GitHub Enterprise URLs

Co-authored-by: Markus Wolf <knister.peter@shadowrun-clan.de>

* 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 <knister.peter@shadowrun-clan.de>

* Add section about using act on GitHub Enterprise to README

Co-authored-by: Markus Wolf <knister.peter@shadowrun-clan.de>

* Set GitHubInstance in runnerConfig in runner_test

Co-authored-by: Markus Wolf <knister.peter@shadowrun-clan.de>

Co-authored-by: hackercat <me@hackerc.at>
Co-authored-by: Markus Wolf <knister.peter@shadowrun-clan.de>
This commit is contained in:
Björn Brauer 2021-05-05 18:42:34 +02:00 committed by GitHub
parent 710a3ac94c
commit 0c4374ec41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 68 additions and 17 deletions

View File

@ -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 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") --env-file string environment file to read and use as env in the containers (default ".env")
-e, --eventpath string path to event JSON file -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 -g, --graph draw workflows
-h, --help help for act -h, --help help for act
--insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs. --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. 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 # Support
Need help? Ask on [Gitter](https://gitter.im/nektos/act)! Need help? Ask on [Gitter](https://gitter.im/nektos/act)!

View File

@ -29,6 +29,7 @@ type Input struct {
containerArchitecture string containerArchitecture string
noWorkflowRecurse bool noWorkflowRecurse bool
useGitIgnore bool useGitIgnore bool
githubInstance string
} }
func (i *Input) resolve(path string) string { func (i *Input) resolve(path string) string {

View File

@ -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().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.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.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()) rootCmd.SetArgs(args())
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
@ -254,6 +255,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
UsernsMode: input.usernsMode, UsernsMode: input.usernsMode,
ContainerArchitecture: input.containerArchitecture, ContainerArchitecture: input.containerArchitecture,
UseGitIgnore: input.useGitIgnore, UseGitIgnore: input.useGitIgnore,
GitHubInstance: input.githubInstance,
} }
r, err := runner.New(config) r, err := runner.New(config)
if err != nil { if err != nil {

View File

@ -15,6 +15,7 @@ import (
git "github.com/go-git/go-git/v5" git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-ini/ini" "github.com/go-ini/ini"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -142,12 +143,12 @@ func findGitPrettyRef(head, root, sub string) (string, error) {
} }
// FindGithubRepo get the repo // FindGithubRepo get the repo
func FindGithubRepo(file string) (string, error) { func FindGithubRepo(file string, githubInstance string) (string, error) {
url, err := findGitRemoteURL(file) url, err := findGitRemoteURL(file)
if err != nil { if err != nil {
return "", err return "", err
} }
_, slug, err := findGitSlug(url) _, slug, err := findGitSlug(url, githubInstance)
return slug, err return slug, err
} }
@ -174,7 +175,7 @@ func findGitRemoteURL(file string) (string, error) {
return url, nil 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 { if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
return "CodeCommit", matches[2], nil return "CodeCommit", matches[2], nil
} else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != 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 return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
} else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil { } else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), 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 return "", url, nil
} }
@ -218,9 +227,10 @@ func findGitDirectory(fromFile string) (string, error) {
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor // NewGitCloneExecutorInput the input for the NewGitCloneExecutor
type NewGitCloneExecutorInput struct { type NewGitCloneExecutorInput struct {
URL string URL string
Ref string Ref string
Dir string Dir string
Token string
} }
// CloneIfRequired ... // CloneIfRequired ...
@ -237,10 +247,24 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
progressWriter = os.Stdout progressWriter = os.Stdout
} }
r, err = git.PlainCloneContext(ctx, input.Dir, false, &git.CloneOptions{ var cloneOptions git.CloneOptions
URL: input.URL, if input.Token != "" {
Progress: progressWriter, 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 { if err != nil {
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err) logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
return nil, err return nil, err

View File

@ -34,7 +34,7 @@ func TestFindGitSlug(t *testing.T) {
} }
for _, tt := range slugTests { for _, tt := range slugTests {
provider, slug, err := findGitSlug(tt.url) provider, slug, err := findGitSlug(tt.url, "github.com")
assert.NoError(err) assert.NoError(err)
assert.Equal(tt.provider, provider) assert.Equal(tt.provider, provider)

View File

@ -503,7 +503,7 @@ func (rc *RunContext) getGithubContext() *githubContext {
} }
repoPath := rc.Config.Workdir repoPath := rc.Config.Workdir
repo, err := common.FindGithubRepo(repoPath) repo, err := common.FindGithubRepo(repoPath, rc.Config.GitHubInstance)
if err != nil { if err != nil {
log.Warningf("unable to get git repo: %v", err) log.Warningf("unable to get git repo: %v", err)
} else { } 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_SERVER_URL"] = "https://github.com"
env["GITHUB_API_URL"] = "https://api.github.com" env["GITHUB_API_URL"] = "https://api.github.com"
env["GITHUB_GRAPHQL_URL"] = "https://api.github.com/graphql" 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() job := rc.Run.Job()
if job.RunsOn() != nil { if job.RunsOn() != nil {

View File

@ -38,6 +38,7 @@ type Config struct {
UsernsMode string // user namespace to use UsernsMode string // user namespace to use
ContainerArchitecture string // Desired OS/architecture platform for running containers ContainerArchitecture string // Desired OS/architecture platform for running containers
UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true 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 // Resolves the equivalent host path inside the container

View File

@ -56,6 +56,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo, sec
ReuseContainers: false, ReuseContainers: false,
ContainerArchitecture: tjfi.containerArchitecture, ContainerArchitecture: tjfi.containerArchitecture,
Secrets: secrets, Secrets: secrets,
GitHubInstance: "github.com",
} }
runner, err := New(runnerConfig) runner, err := New(runnerConfig)

View File

@ -70,7 +70,11 @@ func (sc *StepContext) Executor() common.Executor {
if remoteAction == nil { if remoteAction == nil {
return common.NewErrorExecutor(formatError(step.Uses)) 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 { return func(ctx context.Context) error {
common.Logger(ctx).Debugf("Skipping actions/checkout") common.Logger(ctx).Debugf("Skipping actions/checkout")
return nil return nil
@ -80,9 +84,10 @@ func (sc *StepContext) Executor() common.Executor {
actionDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(step.Uses, "/", "-")) actionDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(step.Uses, "/", "-"))
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{ common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
URL: remoteAction.CloneURL(), URL: remoteAction.CloneURL(),
Ref: remoteAction.Ref, Ref: remoteAction.Ref,
Dir: actionDir, Dir: actionDir,
Token: github.Token,
}), }),
sc.setupAction(actionDir, remoteAction.Path), sc.setupAction(actionDir, remoteAction.Path),
sc.runAction(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 { type remoteAction struct {
URL string
Org string Org string
Repo string Repo string
Path string Path string
@ -575,7 +581,7 @@ type remoteAction struct {
} }
func (ra *remoteAction) CloneURL() string { 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 { func (ra *remoteAction) IsCheckout() bool {
@ -601,6 +607,7 @@ func newRemoteAction(action string) *remoteAction {
Repo: matches[2], Repo: matches[2],
Path: matches[4], Path: matches[4],
Ref: matches[6], Ref: matches[6],
URL: "github.com",
} }
} }