Workflow Conditions¶
Conditions check repository state to determine if workflows or individual actions should execute. They provide fine-grained control over workflow execution based on file existence, file contents, and repository structure.
Condition Levels¶
Conditions can be applied at two levels:
Workflow-Level Conditions¶
Evaluated once per project before any actions execute. If conditions fail, the entire workflow is skipped.
Action-Level Conditions¶
Evaluated before each action executes. If conditions fail, only that specific action is skipped.
[[actions]]
name = "update-dockerfile"
type = "file"
[[actions.conditions]]
file_exists = "Dockerfile"
Condition Types¶
Remote Conditions (Pre-Clone)¶
Remote conditions are checked via API before cloning the repository. They are faster and more efficient than local conditions.
Advantages:
- โก No repository cloning required
- ๐พ Saves bandwidth and disk space
- ๐ Faster workflow evaluation
- โ Early filtering (fail fast)
Limitations:
- Limited to single file content checks
- No glob pattern support for content matching
- API rate limits may apply
remote_file_exists¶
Check if a file exists using GitHub/GitLab API.
Type: string
(file path or glob pattern)
Glob pattern support:
Real-world example:
Why? Workflow migrates projects from setup.cfg to pyproject.toml, so it only runs on projects that still have setup.cfg.
remote_file_not_exists¶
Check if a file does NOT exist using GitHub/GitLab API.
Type: string
(file path or glob pattern)
Real-world example:
Why? Combined with remote_file_exists = "setup.cfg"
, this targets projects that haven't been migrated yet (have setup.cfg but no pyproject.toml).
remote_file_contains + remote_file¶
Check if a file contains specific text or matches a regex pattern.
Type: string
(pattern to search for)
Requires: remote_file
field with target file path
Pattern matching:
- String search first (fast)
- Falls back to regex if string not found
- Use regex escaping:
\\.
for literal.
,\\d
for digits
Example - exact string:
Example - regex pattern:
[[conditions]]
remote_file_contains = "python_requires.*=[\"']>=3\\.(9|10)"
remote_file = "setup.cfg"
remote_file_doesnt_contain + remote_file¶
Check if a file does NOT contain a pattern.
remote_client¶
Specify which API client to use for remote checks.
Type: string
Values: "github"
(default), "gitlab"
Local Conditions (Post-Clone)¶
Local conditions are checked after cloning the repository. They have full filesystem access and support glob patterns.
Advantages:
- โ Full glob pattern support
- โ Access to all files, even .gitignored
- โ Complex pattern matching
- โ Directory checks
Disadvantages:
- ๐ Requires git clone first
- ๐พ Uses bandwidth and disk space
- โฑ๏ธ Slower than remote conditions
file_exists¶
Check if a file or directory exists locally.
Type: ResourceUrl
(path relative to repository)
Supports: Glob patterns
Glob patterns:
[[conditions]]
file_exists = "**/*.py" # Any Python file recursively
[[conditions]]
file_exists = "src/**/__init__.py" # __init__.py in any src subdirectory
Real-world example from example-workflow (action-level):
[[actions]]
name = "extract-constraints"
type = "docker"
command = "extract"
image = "{{ extract_image_from_dockerfile('repository/Dockerfile') }}"
source = "/tmp/constraints.txt"
destination = "extracted:///constraints.txt"
[[actions.conditions]]
file_exists = "Dockerfile"
Why? Only extract Docker constraints if project has a Dockerfile.
file_not_exists¶
Check if a file or directory does NOT exist locally.
Real-world example from example-workflow (action-level):
[[actions]]
name = "extract-original-compose-yml"
type = "git"
command = "extract"
commit_keyword = "migration"
source = "compose.yml"
destination = "extracted:///compose.original.yaml"
[[actions.conditions]]
file_not_exists = "extracted:///compose.original.yaml"
Why? Only attempt to extract compose.yml from git history if we haven't already extracted it from a previous attempt (compose.yaml).
file_contains + file¶
Check if a file contains specific text or matches a regex pattern.
Type: string
(pattern to search for)
Requires: file
field with target file path
Pattern matching:
- String search first (fast)
- Falls back to regex if string not found
- Use regex escaping:
\\.
for literal.
,\\d
for digits
Example - Check Python version:
Example - Check dependencies:
file_doesnt_contain + file¶
Check if a file does NOT contain a pattern.
Condition Evaluation¶
condition_type¶
Controls how multiple conditions are evaluated.
Type: string
Values: "all"
(AND logic), "any"
(OR logic)
Default: "all"
AND Logic (condition_type = "all")¶
ALL conditions must pass for execution to proceed.
condition_type = "all" # All conditions must pass (default)
[[conditions]]
remote_file_exists = "setup.cfg"
[[conditions]]
remote_file_not_exists = "pyproject.toml"
Real-world example:
# Workflow level - targets un-migrated projects
[[conditions]]
remote_file_exists = "setup.cfg"
[[conditions]]
remote_file_not_exists = "pyproject.toml"
Result: Only processes projects that have setup.cfg AND don't have pyproject.toml (haven't been migrated yet).
OR Logic (condition_type = "any")¶
ANY ONE condition passing is sufficient for execution.
condition_type = "any" # Any condition passing is sufficient
[[conditions]]
remote_file_exists = "requirements.txt"
[[conditions]]
remote_file_exists = "pyproject.toml"
[[conditions]]
remote_file_exists = "setup.py"
Result: Executes if project has ANY Python configuration file.
Real-World Examples¶
Example 1: Workflow-Level Conditions (example-workflow)¶
# Only target Python projects that still use setup.cfg
[[conditions]]
remote_file_exists = "setup.cfg"
[[conditions]]
remote_file_not_exists = "pyproject.toml"
What it does:
- โ
Project must have
setup.cfg
(old configuration) - โ
Project must NOT have
pyproject.toml
(not yet migrated)
Why remote conditions? These checks happen before cloning, so we avoid cloning projects that don't need migration. For 1000 projects, this might only clone 50 that need fixing.
Example 2: Action-Level Conditions (Conditional Docker Extraction)¶
[[actions]]
name = "extract-constraints"
type = "docker"
command = "extract"
image = "{{ extract_image_from_dockerfile('repository/Dockerfile') }}"
source = "/tmp/constraints.txt"
destination = "extracted:///constraints.txt"
[[actions.conditions]]
file_exists = "Dockerfile"
What it does:
- Only extracts Docker constraints if project has a Dockerfile
- If no Dockerfile, action is skipped (not a failure)
Why action-level? Not all Python projects use Docker, so this action should only run when applicable.
Example 3: Multiple Action Conditions (Compose File Variations)¶
[[actions]]
name = "extract-original-docker-compose-yml"
type = "git"
command = "extract"
commit_keyword = "migration"
source = "docker-compose.yml"
destination = "extracted:///compose.original.yaml"
ignore_errors = true
[[actions.conditions]]
file_not_exists = "extracted:///compose.original.yaml"
[[actions.conditions]]
file_exists = "repository:///compose.yaml"
What it does:
- โ Only extract if we haven't already extracted a compose file
- โ Only extract if project currently has compose.yaml
Why both conditions? Projects might have compose.yaml, compose.yml, docker-compose.yaml, or docker-compose.yml. The workflow tries each variant in sequence, but stops once one succeeds.
Example 4: Conditional Dockerfile Update¶
[[actions]]
name = "generate-dockerfile"
type = "claude"
prompt = "prompts/dockerfile.md.j2"
validation_prompt = "prompts/validate-dockerfile.md.j2"
[[actions.conditions]]
file_exists = "repository:///Dockerfile"
What it does:
- Only runs Claude to update Dockerfile if project has one
- Projects without Docker are skipped gracefully
Why ResourceUrl? The repository:///
prefix ensures we're checking the cloned repository, not extracted files.
Example 5: Multiple Condition Fallbacks¶
This pattern from example-workflow tries multiple compose file names:
# Try compose.yaml first
[[actions]]
name = "extract-original-compose-yaml"
type = "git"
command = "extract"
source = "compose.yaml"
destination = "extracted:///compose.original.yaml"
# Try compose.yml if compose.yaml wasn't found
[[actions]]
name = "extract-original-compose-yml"
type = "git"
command = "extract"
source = "compose.yml"
destination = "extracted:///compose.original.yaml"
[[actions.conditions]]
file_not_exists = "extracted:///compose.original.yaml" # Only if previous failed
# Try docker-compose.yaml
[[actions]]
name = "extract-original-docker-compose-yaml"
type = "git"
command = "extract"
source = "docker-compose.yaml"
destination = "extracted:///compose.original.yaml"
[[actions.conditions]]
file_not_exists = "extracted:///compose.original.yaml"
# Try docker-compose.yml
[[actions]]
name = "extract-original-docker-compose-yml"
type = "git"
command = "extract"
source = "docker-compose.yml"
destination = "extracted:///compose.original.yaml"
[[actions.conditions]]
file_not_exists = "extracted:///compose.original.yaml"
What it does:
- Try
compose.yaml
(modern name) - If that fails, try
compose.yml
- If that fails, try
docker-compose.yaml
- If that fails, try
docker-compose.yml
- Stop at first success
Why this pattern? Docker Compose supports multiple filenames, and different projects use different conventions. This ensures we find the file regardless of naming.
Best Practices¶
1. Use Remote Conditions First¶
# โ
Good - check remotely before cloning
[[conditions]]
remote_file_exists = "package.json"
[[conditions]]
remote_file_contains = "node.*18"
remote_file = ".nvmrc"
# โ Slower - clones every repository
[[conditions]]
file_exists = "package.json"
[[conditions]]
file_contains = "node.*18"
file = ".nvmrc"
Performance impact: For 1000 projects, remote conditions might process 50, while local conditions require cloning all 1000 first.
2. Combine AND Logic for Precision¶
condition_type = "all" # All must pass (default)
[[conditions]]
remote_file_exists = "Dockerfile"
[[conditions]]
remote_file_contains = "FROM python:3\\.9"
remote_file = "Dockerfile"
[[conditions]]
remote_file_not_exists = "pyproject.toml"
Result: Only Python 3.9 Docker projects without pyproject.toml.
3. Use OR Logic for Flexibility¶
condition_type = "any" # Any one passing is sufficient
[[conditions]]
remote_file_exists = "setup.py"
[[conditions]]
remote_file_exists = "setup.cfg"
[[conditions]]
remote_file_exists = "pyproject.toml"
Result: Any Python project with configuration.
4. Action Conditions for Optional Steps¶
[[actions]]
name = "update-dockerfile"
type = "file"
[[actions.conditions]]
file_exists = "Dockerfile" # Skip if no Docker
[[actions]]
name = "run-tests"
type = "shell"
command = "pytest tests/"
[[actions.conditions]]
file_exists = "tests/" # Skip if no tests
Why? Not all projects need all actions. Conditions allow graceful degradation.
5. Avoid Over-Filtering¶
# โ Too restrictive - might miss valid projects
[[conditions]]
remote_file_contains = "python_requires.*=.*['\"]3\\.9['\"]"
remote_file = "setup.cfg"
# โ
Better - allows variations
[[conditions]]
remote_file_contains = "python.*3\\.9"
remote_file = "setup.cfg"
Why? The second pattern matches more variations in how version might be specified.
Condition vs Filter¶
Feature | Filters | Remote Conditions | Local Conditions |
---|---|---|---|
When evaluated | Before processing | Before cloning | After cloning |
Data source | Imbi metadata | GitHub/GitLab API | Local filesystem |
Speed | โกโกโก Fastest | โกโก Fast | โก Slower |
Use for | Project metadata | File existence/content | Complex patterns |
Glob support | No | Limited | Full |
Bandwidth | None | Minimal | High |
Best practice: Use all three in combination:
- Filters for broad technology targeting
- Remote conditions for file-based applicability
- Local conditions for complex repository checks
Complete Example¶
This is the actual condition strategy from example-workflow:
# Filter: Broad targeting
[filter]
project_types = ["apis", "consumers", ...]
project_facts = {"programming_language" = "Python 3.9"}
github_identifier_required = true
github_workflow_status_exclude = ["success"]
# Workflow conditions: Migration applicability
[[conditions]]
remote_file_exists = "setup.cfg"
[[conditions]]
remote_file_not_exists = "pyproject.toml"
# Action conditions: Optional steps
[[actions]]
name = "extract-constraints"
type = "docker"
[[actions.conditions]]
file_exists = "Dockerfile" # Only if Docker is used
[[actions]]
name = "ensure-correct-pins"
type = "claude"
[[actions.conditions]]
file_exists = "Dockerfile" # Only if Docker is used
[[actions]]
name = "generate-dockerfile"
type = "claude"
[[actions.conditions]]
file_exists = "repository:///Dockerfile" # Only if Dockerfile exists
Result:
- Filter reduces 1000 projects โ 50 Python 3.9 projects with failing builds
- Remote conditions reduce 50 projects โ 30 projects needing migration (have setup.cfg, no pyproject.toml)
- Action conditions skip Docker-related actions for non-Docker projects
See Also¶
- Workflow Filters - Pre-filtering projects by metadata
- Workflow Configuration - Complete configuration reference
- Workflows Overview - High-level concepts and best practices