From 767e6a8696e252e2524d1f29aff86e8d52abd937 Mon Sep 17 00:00:00 2001 From: Shubh Bapna <38372682+shubhbapna@users.noreply.github.com> Date: Sat, 14 Jan 2023 00:58:17 +0530 Subject: [PATCH] Input (#1524) * added input flags * added input as part of the action event and added test cases * updated readme Co-authored-by: ChristopherHX --- README.md | 12 ++++++- cmd/input.go | 7 ++++ cmd/root.go | 34 ++++++++++++++------ pkg/runner/runner.go | 11 +++++++ pkg/runner/runner_test.go | 22 +++++++++++++ pkg/runner/testdata/input-from-cli/input.yml | 21 ++++++++++++ 6 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 pkg/runner/testdata/input-from-cli/input.yml 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 } 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