Shell Actions¶
Shell actions execute arbitrary commands with full Jinja2 template support for dynamic command construction and access to workflow context variables.
Configuration¶
[[actions]]
name = "action-name"
type = "shell"
command = "command to execute"
working_directory = "path" # Optional, default: repository:///
ignore_errors = false # Optional, default: false
Fields¶
command (required)¶
The shell command to execute. Supports full Jinja2 template syntax for variable substitution.
Type: string
Template Variables Available:
workflow
: Workflow configuration objectimbi_project
: Complete Imbi project datagithub_repository
: GitHub repository object (if applicable)working_directory
: Path to workflow working directorystarting_commit
: Initial Git commit SHA
working_directory (optional)¶
Directory to execute the command in.
Type: ResourceUrl
(string path)
Default: repository:///
(the cloned repository directory)
ignore_errors (optional)¶
Whether to continue workflow execution if the command fails (non-zero exit code).
Type: boolean
Default: false
Examples¶
Basic Command Execution¶
Command with Working Directory¶
[[actions]]
name = "build-project"
type = "shell"
command = "python setup.py build"
working_directory = "{{ working_directory }}/repository"
Template Variable Usage¶
[[actions]]
name = "create-tag"
type = "shell"
command = "git tag -a v{{ version }} -m 'Release {{ version }} for {{ imbi_project.name }}'"
working_directory = "repository:///"
Multi-Step Script Execution¶
Note: Shell actions execute commands directly without a shell, so shell operators like &&
, ||
, ;
, and |
do not work. For multi-step operations, use a shell wrapper:
[[actions]]
name = "setup-and-test"
type = "shell"
command = """
bash -c 'python -m venv .venv && source .venv/bin/activate && pip install -e .[dev] && pytest tests/ --cov={{ imbi_project.slug }}'
"""
working_directory = "{{ working_directory }}/repository"
Conditional Execution with Shell¶
[[actions]]
name = "npm-install-if-needed"
type = "shell"
command = "bash -c 'if [ -f package.json ]; then npm install; fi'"
working_directory = "repository:///"
Ignore Errors¶
[[actions]]
name = "optional-linting"
type = "shell"
command = "ruff check src/"
working_directory = "repository:///"
ignore_errors = true # Don't fail workflow if linting fails
Common Use Cases¶
Running Tests¶
[[actions]]
name = "run-python-tests"
type = "shell"
command = "pytest tests/ -v --tb=short"
working_directory = "{{ working_directory }}/repository"
[[actions]]
name = "run-javascript-tests"
type = "shell"
command = "npm test"
working_directory = "repository:///"
Building Artifacts¶
[[actions]]
name = "build-python-package"
type = "shell"
command = "python -m build"
working_directory = "{{ working_directory }}/repository"
[[actions]]
name = "build-docker-image"
type = "shell"
command = "docker build -t {{ imbi_project.slug }}:latest ."
working_directory = "repository:///"
Code Quality Tools¶
[[actions]]
name = "run-linter"
type = "shell"
command = "ruff check --fix src/"
working_directory = "repository:///"
[[actions]]
name = "format-code"
type = "shell"
command = "ruff format src/ tests/"
working_directory = "repository:///"
[[actions]]
name = "type-check"
type = "shell"
command = "mypy src/"
working_directory = "repository:///"
Git Operations¶
[[actions]]
name = "get-current-version"
type = "shell"
command = "git describe --tags --abbrev=0"
working_directory = "repository:///"
[[actions]]
name = "list-changed-files"
type = "shell"
command = "git diff --name-only {{ starting_commit }} HEAD"
working_directory = "{{ working_directory }}/repository"
Environment Setup¶
[[actions]]
name = "setup-python-env"
type = "shell"
command = """
python -m venv .venv && \
.venv/bin/pip install --upgrade pip setuptools wheel
"""
working_directory = "repository:///"
[[actions]]
name = "setup-node-env"
type = "shell"
command = "npm ci"
working_directory = "repository:///"
Advanced Template Examples¶
Using Imbi Project Data¶
[[actions]]
name = "project-specific-command"
type = "shell"
command = """
echo "Processing {{ imbi_project.name }}"
echo "Type: {{ imbi_project.project_type }}"
echo "Namespace: {{ imbi_project.namespace }}"
"""
Conditional Logic with Jinja2¶
[[actions]]
name = "environment-specific-deploy"
type = "shell"
command = """
{% if imbi_project.project_type == 'api' %}
python deploy_api.py
{% elif imbi_project.project_type == 'consumer' %}
python deploy_consumer.py
{% else %}
echo "Unknown project type"
{% endif %}
"""
working_directory = "repository:///"
Using Project Facts¶
[[actions]]
name = "language-specific-test"
type = "shell"
command = """
{% if imbi_project.facts.get('Programming Language') == 'Python 3.12' %}
pytest tests/ --python=3.12
{% elif imbi_project.facts.get('Programming Language') == 'Python 3.11' %}
pytest tests/ --python=3.11
{% endif %}
"""
working_directory = "repository:///"
Iterating Over Lists¶
[[actions]]
name = "install-dependencies"
type = "shell"
command = """
{% for dep in dependencies %}
pip install {{ dep }}
{% endfor %}
"""
working_directory = "repository:///"
Path Resolution¶
Working directory supports all ResourceUrl schemes:
# Repository directory
[[actions]]
type = "shell"
command = "ls -la"
working_directory = "repository:///"
# Workflow directory
[[actions]]
type = "shell"
command = "cat templates/README.md"
working_directory = "workflow:///"
# Extracted files directory
[[actions]]
type = "shell"
command = "find . -name '*.conf'"
working_directory = "extracted:///"
# Explicit working directory path
[[actions]]
type = "shell"
command = "pwd"
working_directory = "{{ working_directory }}/repository"
Command Output¶
Captured Output¶
- stdout: Logged at DEBUG level
- stderr: Logged at DEBUG level
- Exit Code: Non-zero exit codes cause workflow failure (unless
ignore_failure = true
)
Output in Logs¶
# Logger output example:
DEBUG: Executing shell command: pytest tests/ -v
DEBUG: Command stdout: ===== test session starts =====
DEBUG: Command stderr:
DEBUG: Command exit code: 0
Error Handling¶
Exit Code Handling¶
# Fail workflow on error (default)
[[actions]]
name = "critical-command"
type = "shell"
command = "important-operation"
# Fails workflow if exit code != 0
# Continue on error
[[actions]]
name = "optional-command"
type = "shell"
command = "optional-operation"
ignore_errors = true # Continues even if exit code != 0
Command Not Found¶
[[actions]]
name = "missing-command"
type = "shell"
command = "nonexistent-command"
# Raises FileNotFoundError: Command not found: nonexistent-command
Security Considerations¶
Command Injection Prevention¶
Template variables are NOT shell-escaped automatically. Be cautious with user-provided data:
# UNSAFE - if imbi_project.name contains shell metacharacters
[[actions]]
type = "shell"
command = "echo {{ imbi_project.name }}"
# SAFER - use quotes
[[actions]]
type = "shell"
command = "echo '{{ imbi_project.name }}'"
# SAFEST - avoid untrusted input in shell commands
Environment Variables¶
Commands execute with the same environment as the workflow process. Note that environment variables in the command string itself are NOT expanded (no shell):
[[actions]]
name = "use-env-var"
type = "shell"
command = "bash -c 'echo $HOME && echo $USER'" # Need bash -c for shell features
Performance Tips¶
Chaining Commands¶
Important: Shell operators require wrapping the command in bash -c
or sh -c
:
# Use && for dependent commands (fail fast):
[[actions]]
type = "shell"
command = "bash -c 'cd repository && make build && make test'"
# Use ; for independent commands (always run all):
[[actions]]
type = "shell"
command = "bash -c 'make clean; make build; make test'"
Background Processes¶
Not recommended - commands block until completion. For long-running operations, consider using Docker actions instead.
Implementation Notes¶
- Commands execute in a subprocess using
asyncio.create_subprocess_exec
- No shell by default: Commands are parsed with
shlex.split()
and executed directly - Shell features require explicit shell: Use
bash -c '...'
orsh -c '...'
for: - Pipes (
|
), redirects (>
,<
), wildcards (*
) - Command chaining (
&&
,||
,;
) - Environment variable expansion (
$VAR
) - Built-in shell commands (
cd
,export
, etc.) - Working directory resolved before command execution via
utils.resolve_path()
- Template rendering occurs before command execution
- Commands are parsed as shell-like arguments (respecting quotes and escapes)
- No timeout configured (commands run until completion)
- stdout and stderr captured and logged at DEBUG level
- Non-zero exit codes raise
subprocess.CalledProcessError
(unlessignore_errors=true
)