diff --git a/cmd/input.go b/cmd/input.go index bd713037..9b989ed0 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -38,6 +38,9 @@ type Input struct { workflowRecurse bool useGitIgnore bool githubInstance string + gitHubServerURL string + gitHubAPIServerURL string + gitHubGraphQlAPIServerURL string containerCapAdd []string containerCapDrop []string autoRemove bool diff --git a/cmd/root.go b/cmd/root.go index 8c34d70b..27ab04a9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,6 +113,9 @@ func createRootCommand(ctx context.Context, input *Input, version string) *cobra rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "", "URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket)") rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition") rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Only use this when using GitHub Enterprise Server.") + rootCmd.PersistentFlags().StringVarP(&input.gitHubServerURL, "github-server-url", "", "", "Fully qualified URL to the GitHub instance to use with http/https protocol. Only use this when using GitHub Enterprise Server or Gitea.") + rootCmd.PersistentFlags().StringVarP(&input.gitHubAPIServerURL, "github-api-server-url", "", "", "Fully qualified URL to the GitHub instance api url to use with http/https protocol. Only use this when using GitHub Enterprise Server or Gitea.") + rootCmd.PersistentFlags().StringVarP(&input.gitHubGraphQlAPIServerURL, "github-graph-ql-api-server-url", "", "", "Fully qualified URL to the GitHub instance graphql api to use with http/https protocol. Only use this when using GitHub Enterprise Server or Gitea.") rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.") rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.") rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.") @@ -630,6 +633,9 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str ContainerOptions: input.containerOptions, UseGitIgnore: input.useGitIgnore, GitHubInstance: input.githubInstance, + GitHubServerURL: input.gitHubServerURL, + GitHubAPIServerURL: input.gitHubAPIServerURL, + GitHubGraphQlAPIServerURL: input.gitHubGraphQlAPIServerURL, ContainerCapAdd: input.containerCapAdd, ContainerCapDrop: input.containerCapDrop, AutoRemove: input.autoRemove, diff --git a/pkg/runner/action_composite.go b/pkg/runner/action_composite.go index bdd87cce..fb2ab2eb 100644 --- a/pkg/runner/action_composite.go +++ b/pkg/runner/action_composite.go @@ -76,6 +76,14 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action EventJSON: parent.EventJSON, nodeToolFullPath: parent.nodeToolFullPath, } + if parent.ContextData != nil { + compositerc.ContextData = map[string]interface{}{} + for k, v := range parent.ContextData { + if !strings.EqualFold("inputs", k) { + compositerc.ContextData[k] = v + } + } + } compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx) return compositerc diff --git a/pkg/runner/expression.go b/pkg/runner/expression.go index 703b0ea0..5214371f 100644 --- a/pkg/runner/expression.go +++ b/pkg/runner/expression.go @@ -90,6 +90,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map Needs: using, Inputs: inputs, HashFiles: getHashFilesFunction(ctx, rc), + CtxData: rc.ContextData, } if rc.JobContainer != nil { ee.Runner = rc.JobContainer.GetRunnerContext(ctx) @@ -155,9 +156,11 @@ func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, // but required to interpolate/evaluate the inputs in actions/composite Inputs: inputs, HashFiles: getHashFilesFunction(ctx, rc), + CtxData: rc.ContextData, } if rc.JobContainer != nil { ee.Runner = rc.JobContainer.GetRunnerContext(ctx) + ee.EnvCS = !rc.JobContainer.IsEnvironmentCaseInsensitive() } return expressionEvaluator{ interpreter: exprparser.NewInterpeter(ee, exprparser.Config{ diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 57569d96..4cff096a 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -53,6 +53,7 @@ type RunContext struct { cleanUpJobContainer common.Executor caller *caller // job calling this RunContext (reusable workflows) Cancelled bool + ContextData map[string]interface{} nodeToolFullPath string } @@ -959,6 +960,8 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext ghc.Workspace = rc.JobContainer.ToContainerPath(rc.Config.Workdir) } + rc.mergeGitHubContextWithContextData(ghc) + if ghc.RunAttempt == "" { ghc.RunAttempt = "1" } @@ -994,7 +997,7 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext ghc.SetBaseAndHeadRef() repoPath := rc.Config.Workdir - ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath) + ghc.SetRepositoryAndOwner(ctx, rc.Config.GetGitHubInstance(), rc.Config.RemoteName, repoPath) if ghc.Ref == "" { ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath) } @@ -1004,16 +1007,16 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext ghc.SetRefTypeAndName() - // defaults - ghc.ServerURL = "https://github.com" - ghc.APIURL = "https://api.github.com" - ghc.GraphQLURL = "https://api.github.com/graphql" - // per GHES - if rc.Config.GitHubInstance != "github.com" { - ghc.ServerURL = fmt.Sprintf("https://%s", rc.Config.GitHubInstance) - ghc.APIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance) - ghc.GraphQLURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance) + if ghc.ServerURL == "" { + ghc.ServerURL = rc.Config.GetGitHubServerURL() } + if ghc.APIURL == "" { + ghc.APIURL = rc.Config.GetGitHubAPIServerURL() + } + if ghc.GraphQLURL == "" { + ghc.GraphQLURL = rc.Config.GetGitHubGraphQlAPIServerURL() + } + // allow to be overridden by user if rc.Config.Env["GITHUB_SERVER_URL"] != "" { ghc.ServerURL = rc.Config.Env["GITHUB_SERVER_URL"] @@ -1028,6 +1031,25 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext return ghc } +func (rc *RunContext) mergeGitHubContextWithContextData(ghc *model.GithubContext) { + if rnout, ok := rc.ContextData["github"]; ok { + nout, ok := rnout.(map[string]interface{}) + if ok { + var out map[string]interface{} + content, _ := json.Marshal(ghc) + _ = json.Unmarshal(content, &out) + for k, v := range nout { + // gitea sends empty string github contextdata, which replaced github.workspace + if v != nil && v != "" { + out[k] = v + } + } + content, _ = json.Marshal(out) + _ = json.Unmarshal(content, &ghc) + } + } +} + func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool { if step.Type() == model.StepTypeInvalid { // This will be errored out by the executor later, we need this here to avoid a null panic though diff --git a/pkg/runner/run_context_test.go b/pkg/runner/run_context_test.go index f5609ff1..d4b94285 100644 --- a/pkg/runner/run_context_test.go +++ b/pkg/runner/run_context_test.go @@ -393,6 +393,54 @@ func TestGetGitHubContext(t *testing.T) { assert.Equal(t, "job1", ghc.Job) } +func TestGetGitHubContextOverlay(t *testing.T) { + log.SetLevel(log.DebugLevel) + + cwd, err := os.Getwd() + assert.Nil(t, err) + + rc := &RunContext{ + Config: &Config{ + EventName: "push", + Workdir: cwd, + }, + Run: &model.Run{ + Workflow: &model.Workflow{ + Name: "GitHubContextTest", + }, + }, + Name: "GitHubContextTest", + CurrentStep: "step", + Matrix: map[string]interface{}{}, + Env: map[string]string{}, + ExtraPath: []string{}, + StepResults: map[string]*model.StepResult{}, + OutputMappings: map[MappableOutput]MappableOutput{}, + ContextData: map[string]interface{}{ + "github": map[string]interface{}{ + "actor": "me", + "repository": "myowner/myrepo", + "repository_owner": "myowner", + }, + }, + } + rc.Run.JobID = "job1" + + ghc := rc.getGithubContext(context.Background()) + + log.Debugf("%v", ghc) + + assert.Equal(t, "1", ghc.RunID) + assert.Equal(t, "1", ghc.RunNumber) + assert.Equal(t, "0", ghc.RetentionDays) + assert.Equal(t, "me", ghc.Actor) + assert.Equal(t, "myowner/myrepo", ghc.Repository) + assert.Equal(t, "myowner", ghc.RepositoryOwner) + assert.Equal(t, "/dev/null", ghc.RunnerPerflog) + assert.Equal(t, rc.Config.Secrets["GITHUB_TOKEN"], ghc.Token) + assert.Equal(t, "job1", ghc.Job) +} + func TestGetGithubContextRef(t *testing.T) { table := []struct { event string diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index a432f63f..788dba2d 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "regexp" "runtime" "github.com/actions-oss/act-cli/pkg/common" @@ -48,6 +49,9 @@ type Config struct { ContainerOptions string // Options for the job container UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true GitHubInstance string // GitHub instance to use, default "github.com" + GitHubServerURL string // GitHub server url to use + GitHubAPIServerURL string // GitHub api server url to use + GitHubGraphQlAPIServerURL string // GitHub graphql server url to use 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 @@ -64,6 +68,38 @@ type Config struct { HostEnvironmentDir string // Custom folder for host environment, parallel jobs must be 1 } +func (runnerConfig *Config) GetGitHubServerURL() string { + if len(runnerConfig.GitHubServerURL) > 0 { + return runnerConfig.GitHubServerURL + } + return fmt.Sprintf("https://%s", runnerConfig.GitHubInstance) +} +func (runnerConfig *Config) GetGitHubAPIServerURL() string { + if len(runnerConfig.GitHubAPIServerURL) > 0 { + return runnerConfig.GitHubAPIServerURL + } + if runnerConfig.GitHubInstance == "github.com" { + return "https://api.github.com" + } + return fmt.Sprintf("https://%s/api/v3", runnerConfig.GitHubInstance) +} +func (runnerConfig *Config) GetGitHubGraphQlAPIServerURL() string { + if len(runnerConfig.GitHubGraphQlAPIServerURL) > 0 { + return runnerConfig.GitHubGraphQlAPIServerURL + } + if runnerConfig.GitHubInstance == "github.com" { + return "https://api.github.com/graphql" + } + return fmt.Sprintf("https://%s/api/graphql", runnerConfig.GitHubInstance) +} +func (runnerConfig *Config) GetGitHubInstance() string { + if len(runnerConfig.GitHubServerURL) > 0 { + regex := regexp.MustCompile("^https?://(.*)$") + return regex.ReplaceAllString(runnerConfig.GitHubServerURL, "$1") + } + return runnerConfig.GitHubInstance +} + type caller struct { runContext *RunContext } diff --git a/pkg/runner/step_action_remote_test.go b/pkg/runner/step_action_remote_test.go index 9fcbb76d..bf9d93bf 100644 --- a/pkg/runner/step_action_remote_test.go +++ b/pkg/runner/step_action_remote_test.go @@ -55,7 +55,10 @@ func TestStepActionRemote(t *testing.T) { read bool run bool } - runError error + runError error + gitHubServerURL string + gitHubAPIServerURL string + gitHubGraphQlAPIServerURL string }{ { name: "run-successful", @@ -80,6 +83,32 @@ func TestStepActionRemote(t *testing.T) { run: true, }, }, + { + name: "run-successful", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + result: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: true, + read: true, + run: true, + }, + gitHubServerURL: "http://localhost:3000", + gitHubAPIServerURL: "http://localhost:3000/api/v1", + gitHubGraphQlAPIServerURL: "http://localhost:3000/api/graphql", + }, { name: "run-skipped", stepModel: &model.Step{ @@ -142,8 +171,11 @@ func TestStepActionRemote(t *testing.T) { sar := &stepActionRemote{ RunContext: &RunContext{ Config: &Config{ - GitHubInstance: "github.com", - ActionCache: cacheMock, + GitHubInstance: "github.com", + ActionCache: cacheMock, + GitHubServerURL: tt.gitHubServerURL, + GitHubAPIServerURL: tt.gitHubAPIServerURL, + GitHubGraphQlAPIServerURL: tt.gitHubGraphQlAPIServerURL, }, Run: &model.Run{ JobID: "1", @@ -162,7 +194,12 @@ func TestStepActionRemote(t *testing.T) { } sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx) - cacheMock.Mock.On("Fetch", ctx, mock.AnythingOfType("string"), "https://github.com/remote/action", "v1", "").Return("someval") + serverURL := "https://github.com" + if tt.gitHubServerURL != "" { + serverURL = tt.gitHubServerURL + } + + cacheMock.Mock.On("Fetch", ctx, mock.AnythingOfType("string"), serverURL+"/remote/action", "v1", "").Return("someval") suffixMatcher := func(suffix string) interface{} { return mock.MatchedBy(func(actionDir string) bool { return strings.HasSuffix(actionDir, suffix) @@ -173,7 +210,9 @@ func TestStepActionRemote(t *testing.T) { sarm.Mock.On("readAction", sar.Step, "someval", "", mock.Anything, mock.Anything).Return(&model.Action{}, nil) } if tt.mocks.run { - sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), newRemoteAction(sar.Step.Uses)).Return(func(_ context.Context) error { return tt.runError }) + remoteAction := newRemoteAction(sar.Step.Uses) + remoteAction.URL = serverURL + sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), remoteAction).Return(func(_ context.Context) error { return tt.runError }) cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(_ context.Context) error { return nil