feat: contextdata overlay api and more gh/gt instance flags (#105)

* add TestGetGitHubContextOverlay
This commit is contained in:
ChristopherHX
2025-05-18 13:44:33 +02:00
committed by GitHub
parent 6440a419d2
commit b634fba677
8 changed files with 180 additions and 15 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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{

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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