From 636c8a34aedf9b6251df6729fe673d828f972030 Mon Sep 17 00:00:00 2001
From: Shubh Bapna <38372682+shubhbapna@users.noreply.github.com>
Date: Sun, 19 Mar 2023 13:25:55 -0400
Subject: [PATCH] feat: specify matrix on command line (#1675)

* added matrix option

* select the correct subset of matrix configuration after producing all the matrix configuration

* add tests

* update readme

* lint fix

* remove matrix from readme

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 cmd/input.go                                  |  1 +
 cmd/root.go                                   | 23 +++++
 pkg/runner/runner.go                          | 93 ++++++++++++-------
 pkg/runner/runner_test.go                     | 28 ++++++
 .../matrix-with-user-inclusions/push.yml      | 34 +++++++
 5 files changed, 144 insertions(+), 35 deletions(-)
 create mode 100644 pkg/runner/testdata/matrix-with-user-inclusions/push.yml

diff --git a/cmd/input.go b/cmd/input.go
index 37655a5..9327de2 100644
--- a/cmd/input.go
+++ b/cmd/input.go
@@ -47,6 +47,7 @@ type Input struct {
 	remoteName                         string
 	replaceGheActionWithGithubCom      []string
 	replaceGheActionTokenWithGithubCom string
+	matrix                             []string
 }
 
 func (i *Input) resolve(path string) string {
diff --git a/cmd/root.go b/cmd/root.go
index e5c0479..9983a4b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -66,6 +66,7 @@ func Execute(ctx context.Context, version string) {
 	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.Flags().StringArrayVarP(&input.matrix, "matrix", "", []string{}, "specify which matrix configuration to include (e.g. --matrix java:13")
 	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")
@@ -295,6 +296,24 @@ func readEnvs(path string, envs map[string]string) bool {
 	return false
 }
 
+func parseMatrix(matrix []string) map[string]map[string]bool {
+	// each matrix entry should be of the form - string:string
+	r := regexp.MustCompile(":")
+	matrixes := make(map[string]map[string]bool)
+	for _, m := range matrix {
+		matrix := r.Split(m, 2)
+		if len(matrix) < 2 {
+			log.Fatalf("Invalid matrix format. Failed to parse %s", m)
+		} else {
+			if _, ok := matrixes[matrix[0]]; !ok {
+				matrixes[matrix[0]] = make(map[string]bool)
+			}
+			matrixes[matrix[0]][matrix[1]] = true
+		}
+	}
+	return matrixes
+}
+
 //nolint:gocyclo
 func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
@@ -329,6 +348,9 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 		secrets := newSecrets(input.secrets)
 		_ = readEnvs(input.Secretfile(), secrets)
 
+		matrixes := parseMatrix(input.matrix)
+		log.Debugf("Evaluated matrix inclusions: %v", matrixes)
+
 		planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse)
 		if err != nil {
 			return err
@@ -508,6 +530,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 			RemoteName:                         input.remoteName,
 			ReplaceGheActionWithGithubCom:      input.replaceGheActionWithGithubCom,
 			ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom,
+			Matrix:                             matrixes,
 		}
 		r, err := runner.New(config)
 		if err != nil {
diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go
index 7e3f9b1..715d158 100644
--- a/pkg/runner/runner.go
+++ b/pkg/runner/runner.go
@@ -20,40 +20,41 @@ 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
-	Inputs                             map[string]string // manually passed action inputs
-	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
-	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"
-	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
-	ArtifactServerAddr                 string            // the address the artifact server binds to
-	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.
+	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
+	Inputs                             map[string]string          // manually passed action inputs
+	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
+	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"
+	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
+	ArtifactServerAddr                 string                     // the address the artifact server binds to
+	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.
+	Matrix                             map[string]map[string]bool // Matrix config to run
 }
 
 type caller struct {
@@ -116,7 +117,10 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
 						log.Errorf("Error while evaluating matrix: %v", err)
 					}
 				}
-				matrixes := job.GetMatrixes()
+
+				matrixes := selectMatrixes(job.GetMatrixes(), runner.config.Matrix)
+				log.Debugf("Final matrix after applying user inclusions '%v'", matrixes)
+
 				maxParallel := 4
 				if job.Strategy != nil {
 					maxParallel = job.Strategy.MaxParallel
@@ -171,6 +175,25 @@ func handleFailure(plan *model.Plan) common.Executor {
 	}
 }
 
+func selectMatrixes(originalMatrixes []map[string]interface{}, targetMatrixValues map[string]map[string]bool) []map[string]interface{} {
+	matrixes := make([]map[string]interface{}, 0)
+	for _, original := range originalMatrixes {
+		flag := true
+		for key, val := range original {
+			if allowedVals, ok := targetMatrixValues[key]; ok {
+				valToString := fmt.Sprintf("%v", val)
+				if _, ok := allowedVals[valToString]; !ok {
+					flag = false
+				}
+			}
+		}
+		if flag {
+			matrixes = append(matrixes, original)
+		}
+	}
+	return matrixes
+}
+
 func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]interface{}) *RunContext {
 	rc := &RunContext{
 		Config:      runner.config,
diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go
index 60a8193..f8468bb 100644
--- a/pkg/runner/runner_test.go
+++ b/pkg/runner/runner_test.go
@@ -186,6 +186,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
 		Inputs:                cfg.Inputs,
 		GitHubInstance:        "github.com",
 		ContainerArchitecture: cfg.ContainerArchitecture,
+		Matrix:                cfg.Matrix,
 	}
 
 	runner, err := New(runnerConfig)
@@ -584,3 +585,30 @@ func TestRunEventPullRequest(t *testing.T) {
 
 	tjfi.runTest(context.Background(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")})
 }
+
+func TestRunMatrixWithUserDefinedInclusions(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+	workflowPath := "matrix-with-user-inclusions"
+
+	tjfi := TestJobFileInfo{
+		workdir:      workdir,
+		workflowPath: workflowPath,
+		eventName:    "push",
+		errorMessage: "",
+		platforms:    platforms,
+	}
+
+	matrix := map[string]map[string]bool{
+		"node": {
+			"8":   true,
+			"8.x": true,
+		},
+		"os": {
+			"ubuntu-18.04": true,
+		},
+	}
+
+	tjfi.runTest(context.Background(), t, &Config{Matrix: matrix})
+}
diff --git a/pkg/runner/testdata/matrix-with-user-inclusions/push.yml b/pkg/runner/testdata/matrix-with-user-inclusions/push.yml
new file mode 100644
index 0000000..2fd19b4
--- /dev/null
+++ b/pkg/runner/testdata/matrix-with-user-inclusions/push.yml
@@ -0,0 +1,34 @@
+name: matrix-with-user-inclusions
+on: push
+
+jobs:
+  build:
+    name: PHP ${{ matrix.os }} ${{ matrix.node}}
+    runs-on: ubuntu-latest
+    steps:
+      - run: |
+          echo ${NODE_VERSION} | grep 8
+          echo ${OS_VERSION} | grep ubuntu-18.04
+        env:
+          NODE_VERSION: ${{ matrix.node }}
+          OS_VERSION: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-18.04, macos-latest]
+        node: [4, 6, 8, 10]
+        exclude:
+          - os: macos-latest
+            node: 4
+        include:
+          - os: ubuntu-16.04
+            node: 10
+
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node: [8.x, 10.x, 12.x, 13.x]
+    steps:
+      - run: echo ${NODE_VERSION} | grep 8.x
+        env:
+          NODE_VERSION: ${{ matrix.node }}