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