Repository: nektos/act (language: go)

Issue:
Enhancement: A flag for providing inputs
Would be great if I could say `--input name=value` instead of having to create a file as per https://github.com/nektos/act/issues/332.

I am using GitHub Actions for tasks like "deploy a certain branch to staging", etc, and nearly all my actions currently require inputs. (While, on the other hand, I don't have any `on: push` actions.)


Discussion / hints:
I second this. It would be super useful.
@catthehacker @KnisterPeter  🔝  what do you think? I'm using this library for the first time and this is the first thing I noticed is missing. 

Thanks!
@Yago-Ferrer feel free to start implementing and contributing it
If issue is not closed, it's free to implement. If it's not implemented yet, it's most likely up to you to do it (until we fix all the major issues and problems with higher priority :).
It would be great if act aligned with github's workflow command in `gh` tool: https://cli.github.com/manual/gh_workflow_run Specifically, the `-f`, `-F`, and `--json` options for specifying inputs.
I would like to take this issue if no one is working on it

Fix patch:
diff --git a/README.md b/README.md
index 7f9d962b29d..f8577ee5966 100644
--- a/README.md
+++ b/README.md
@@ -186,6 +186,8 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co
       --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
   -h, --help                                        help for act
+      --input stringArray                           action input to make available to actions (e.g. --input myinput=foo)
+      --input-file string                           input file to read and use as action input (default ".input")
       --insecure-secrets                            NOT RECOMMENDED! Doesn't hide secrets while printing logs.
   -j, --job string                                  run job
   -l, --list                                        list workflows
@@ -408,7 +410,7 @@ act pull_request -e pull-request.json
 
 Act will properly provide `github.head_ref` and `github.base_ref` to the action as expected.
 
-## Pass Inputs to Manually Triggered Workflows
+# Pass Inputs to Manually Triggered Workflows
 
 Example workflow file
 
@@ -434,6 +436,14 @@ jobs:
           echo "Hello ${{ github.event.inputs.NAME }} and ${{ github.event.inputs.SOME_VALUE }}!"
 ```
 
+## via input or input-file flag
+
+- `act --input NAME=somevalue` - use `somevalue` as the value for `NAME` input.
+- `act --input-file my.input` - load input values from `my.input` file.
+  - input file format is the same as `.env` format
+
+## via JSON
+
 Example JSON payload file conveniently named `payload.json`
 
 ```json
diff --git a/cmd/input.go b/cmd/input.go
index f17fdfceb1e..1caeccbb74e 100644
--- a/cmd/input.go
+++ b/cmd/input.go
@@ -17,12 +17,14 @@ type Input struct {
 	bindWorkdir                        bool
 	secrets                            []string
 	envs                               []string
+	inputs                             []string
 	platforms                          []string
 	dryrun                             bool
 	forcePull                          bool
 	forceRebuild                       bool
 	noOutput                           bool
 	envfile                            string
+	inputfile                          string
 	secretfile                         string
 	insecureSecrets                    bool
 	defaultBranch                      string
@@ -84,3 +86,8 @@ func (i *Input) WorkflowsPath() string {
 func (i *Input) EventPath() string {
 	return i.resolve(i.eventPath)
 }
+
+// Inputfile returns the path to the input file
+func (i *Input) Inputfile() string {
+	return i.resolve(i.inputfile)
+}
diff --git a/cmd/root.go b/cmd/root.go
index 92ae8732d8c..0aa56e782b8 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -47,6 +47,7 @@ func Execute(ctx context.Context, version string) {
 	rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo")
 	rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
 	rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
+	rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)")
 	rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
 	rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs")
 	rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy")
@@ -74,6 +75,7 @@ func Execute(ctx context.Context, version string) {
 	rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
 	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.inputfile, "input-file", "", ".input", "input file to read and use as action input")
 	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.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")
 	rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition")
@@ -249,6 +251,21 @@ func setupLogging(cmd *cobra.Command, _ []string) {
 	}
 }
 
+func parseEnvs(env []string, envs map[string]string) bool {
+	if env != nil {
+		for _, envVar := range env {
+			e := strings.SplitN(envVar, `=`, 2)
+			if len(e) == 2 {
+				envs[e[0]] = e[1]
+			} else {
+				envs[e[0]] = ""
+			}
+		}
+		return true
+	}
+	return false
+}
+
 func readEnvs(path string, envs map[string]string) bool {
 	if _, err := os.Stat(path); err == nil {
 		env, err := godotenv.Read(path)
@@ -285,18 +302,14 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 
 		log.Debugf("Loading environment from %s", input.Envfile())
 		envs := make(map[string]string)
-		if input.envs != nil {
-			for _, envVar := range input.envs {
-				e := strings.SplitN(envVar, `=`, 2)
-				if len(e) == 2 {
-					envs[e[0]] = e[1]
-				} else {
-					envs[e[0]] = ""
-				}
-			}
-		}
+		_ = parseEnvs(input.envs, envs)
 		_ = readEnvs(input.Envfile(), envs)
 
+		log.Debugf("Loading action inputs from %s", input.Inputfile())
+		inputs := make(map[string]string)
+		_ = parseEnvs(input.inputs, inputs)
+		_ = readEnvs(input.Inputfile(), inputs)
+
 		log.Debugf("Loading secrets from %s", input.Secretfile())
 		secrets := newSecrets(input.secrets)
 		_ = readEnvs(input.Secretfile(), secrets)
@@ -444,6 +457,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 			JSONLogger:                         input.jsonLogger,
 			Env:                                envs,
 			Secrets:                            secrets,
+			Inputs:                             inputs,
 			Token:                              secrets["GITHUB_TOKEN"],
 			InsecureSecrets:                    input.insecureSecrets,
 			Platforms:                          input.newPlatforms(),
diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go
index 2ed967db160..9eb225fb66c 100644
--- a/pkg/runner/runner.go
+++ b/pkg/runner/runner.go
@@ -2,6 +2,7 @@ package runner
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"os"
 
@@ -31,6 +32,7 @@ type Config struct {
 	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
@@ -81,6 +83,15 @@ func (runner *runnerImpl) configure() (Runner, error) {
 			return nil, err
 		}
 		runner.eventJSON = string(eventJSONBytes)
+	} else if len(runner.config.Inputs) != 0 {
+		eventMap := map[string]map[string]string{
+			"inputs": runner.config.Inputs,
+		}
+		eventJSON, err := json.Marshal(eventMap)
+		if err != nil {
+			return nil, err
+		}
+		runner.eventJSON = string(eventJSON)
 	}
 	return runner, nil
 }


Test patch:
diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go
index 95fbbcc2175..6096abe1adc 100644
--- a/pkg/runner/runner_test.go
+++ b/pkg/runner/runner_test.go
@@ -94,6 +94,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
 		ReuseContainers:       false,
 		Env:                   cfg.Env,
 		Secrets:               cfg.Secrets,
+		Inputs:                cfg.Inputs,
 		GitHubInstance:        "github.com",
 		ContainerArchitecture: cfg.ContainerArchitecture,
 	}
@@ -419,6 +420,27 @@ func TestRunEventSecrets(t *testing.T) {
 	tjfi.runTest(context.Background(), t, &Config{Secrets: secrets, Env: env})
 }
 
+func TestRunActionInputs(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+	workflowPath := "input-from-cli"
+
+	tjfi := TestJobFileInfo{
+		workdir:      workdir,
+		workflowPath: workflowPath,
+		eventName:    "workflow_dispatch",
+		errorMessage: "",
+		platforms:    platforms,
+	}
+
+	inputs := map[string]string{
+		"SOME_INPUT": "input",
+	}
+
+	tjfi.runTest(context.Background(), t, &Config{Inputs: inputs})
+}
+
 func TestRunEventPullRequest(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skipping integration test")
diff --git a/pkg/runner/testdata/input-from-cli/input.yml b/pkg/runner/testdata/input-from-cli/input.yml
new file mode 100644
index 00000000000..42d3460f3ae
--- /dev/null
+++ b/pkg/runner/testdata/input-from-cli/input.yml
@@ -0,0 +1,21 @@
+on:
+  workflow_dispatch:
+    inputs:
+      NAME:
+        description: "A random input name for the workflow"
+        type: string
+        required: true
+      SOME_VALUE:
+        description: "Some other input to pass"
+        type: string
+        required: true
+
+jobs:
+  test:
+    name: Test
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Test with inputs
+        run: |
+          [ -z "${{ github.event.inputs.SOME_INPUT }}" ] && exit 1 || exit 0