Using `act --var` and `--var-file` to Pass Workflow Variables in Local GitHub Actions
The act CLI accepts workflow variables via --var KEY=VALUE flags or --var-file path files, merging them into a Vars map that workflows access via ${{ vars.KEY }} syntax.
Running GitHub Actions locally with nektos/act often requires injecting dynamic values without hardcoding them into workflow YAML. The --var and --var-file flags solve this by piping custom variables into the runner's expression engine, letting you test workflows against different environments or secrets managers without committing sensitive data.
How act Processes Variable Flags Internally
The implementation spans the command layer and the runner engine. Here is the exact path your variables take from CLI argument to workflow expression.
Flag Registration and Input Handling
The root command defines the interface in cmd/root.go. Line 79 registers --var as a string array, while line 109 registers --var-file as a single string path.
// cmd/root.go:79
rootCmd.PersistentFlags().StringArrayVarP(&input.vars, "var", "", []string{}, "")
// cmd/root.go:109
rootCmd.PersistentFlags().StringVarP(&input.varfile, "var-file", "", "", "")
These values populate the Input struct defined in cmd/input.go:18-20, which holds the raw slices before validation.
type Input struct {
vars []string
varfile string
// ...
}
When the CLI resolves the file path, the Varfile() method (lines 94-96) interprets relative paths against the working directory, ensuring the subsequent loader finds the file regardless of where you invoke act.
Parsing and Loading Variable Definitions
act reuses existing environment-parsing machinery for variable files. The readEnvs function (called at cmd/root.go:445-447) ingests the file specified by --var-file, supporting both .env style (KEY=VALUE) and YAML formats, and returns a map[string]string.
Individual --var arguments are processed by newSecrets in cmd/secrets.go:14-36. Despite the name, this helper parses both secrets and variables. It splits each NAME[=VALUE] entry, upper-cases the key, and—if the value is omitted—reads from the host environment or prompts interactively.
Merging CLI and File Variables
Precedence is determined by load order. First, readEnvs loads the file content into a map. Then, newSecrets iterates over the --var slice and writes into the same map. Because the CLI processing happens second, command-line variables override file definitions when keys collide.
Runtime Exposure via the Vars Map
Once merged, the map is stored in runner.Config.Vars. The Config struct definition in pkg/runner/runner.go:38-41 includes:
type Config struct {
Vars map[string]string
// ...
}
During workflow execution, the expression evaluator calls getWorkflowVars (located at pkg/runner/expression.go:92-94), which returns rc.Config.Vars. This makes every entry available to the workflow expression engine as ${{ vars.NAME }}.
Practical Examples for Local Testing
Pass a Single Variable Inline
Supply a build flag without touching the workflow file:
act -j build --var BUILD_TYPE=debug
Inside the workflow, reference it via the expression context:
- run: echo "Building ${{ vars.BUILD_TYPE }} image"
Load Multiple Variables from a File
Create staging.vars:
API_ENDPOINT=https://staging.example.com
TIMEOUT_SECONDS=30
DEBUG_MODE=true
Execute the job with the file path:
act -j deploy --var-file staging.vars
The runner loads all three entries into the Vars map, making ${{ vars.API_ENDPOINT }} and siblings available immediately.
Override File Values with CLI Flags
When you need to tweak one value without editing the file, combine both flags. The CLI value wins:
# staging.vars contains LOG_LEVEL=info
act -j test --var-file staging.vars --var LOG_LEVEL=debug
LOG_LEVEL resolves to debug because the --var assignment overwrites the file entry during the merge step.
Access Variables in Workflow Expressions and Shell
Variables appear in both the expression context and the shell environment:
jobs:
example:
runs-on: ubuntu-latest
steps:
- name: Via expression syntax
run: echo "Endpoint is ${{ vars.API_ENDPOINT }}"
- name: Via shell environment
run: echo "Timeout is $TIMEOUT_SECONDS"
Both methods work because the runner injects the Vars map into the step's environment variables in addition to the expression evaluator.
Summary
--varaccepts individualKEY=VALUEpairs and can be repeated; missing values fall back to host environment variables or interactive prompts.--var-filepoints to a.envor YAML file containing multiple definitions, resolved relative to the working directory.- Precedence: CLI flags override file entries when names conflict, as implemented by sequential map writes in
cmd/root.go. - Access: Variables surface in workflows as
${{ vars.NAME }}viagetWorkflowVarsinpkg/runner/expression.go, and also propagate to shell environments for$NAMEaccess.
Frequently Asked Questions
Does act --var support reading values from the host environment?
Yes. If you omit the value in a --var argument (e.g., --var MY_TOKEN), the newSecrets parser in cmd/secrets.go:14-36 looks for MY_TOKEN in the host environment. If it is not found and stdin is a TTY, act prompts you interactively to enter the value.
Can I use YAML format instead of .env style in var files?
Yes. The readEnvs function (used at cmd/root.go:445-447) accepts both formats. You can use traditional KEY=VALUE lines or a YAML mapping structure; the parser normalizes either into the internal map[string]string required by the runner configuration.
What happens if the same variable is defined in both --var-file and --var?
The --var value takes precedence. The loading sequence in cmd/root.go first populates the map from the file via readEnvs, then processes the CLI slice via newSecrets. Because the latter writes after the former, duplicate keys are overwritten by the command-line specification.
How do I list or debug which variables are actually loaded?
act does not print the Vars map by default. To verify injection, add a step that dumps the context:
- run: echo '${{ toJson(vars) }}'
This outputs the entire Vars object as JSON during the run, letting you confirm that act --var and --var-file merged correctly before your production logic executes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →