Skip to content

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.

[[conditions]]
remote_file_exists = "setup.cfg"

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)

[[conditions]]
remote_file_exists = "setup.cfg"

Glob pattern support:

[[conditions]]
remote_file_exists = "**/*.tf"  # Any Terraform file recursively

Real-world example:

[[conditions]]
remote_file_exists = "setup.cfg"

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)

[[conditions]]
remote_file_not_exists = "pyproject.toml"

Real-world example:

[[conditions]]
remote_file_not_exists = "pyproject.toml"

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

[[conditions]]
remote_file_contains = "python.*3\\.9"
remote_file = "setup.cfg"

Pattern matching:

  1. String search first (fast)
  2. Falls back to regex if string not found
  3. Use regex escaping: \\. for literal ., \\d for digits

Example - exact string:

[[conditions]]
remote_file_contains = "FROM python:3.9"
remote_file = "Dockerfile"

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.

[[conditions]]
remote_file_doesnt_contain = "python.*3\\.12"
remote_file = "pyproject.toml"

remote_client

Specify which API client to use for remote checks.

Type: string

Values: "github" (default), "gitlab"

[[conditions]]
remote_client = "gitlab"
remote_file_exists = ".gitlab-ci.yml"

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

[[conditions]]
file_exists = "Dockerfile"

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.

[[conditions]]
file_not_exists = ".travis.yml"  # No legacy CI

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

[[conditions]]
file_contains = "FROM python:3\\.9"
file = "Dockerfile"

Pattern matching:

  1. String search first (fast)
  2. Falls back to regex if string not found
  3. Use regex escaping: \\. for literal ., \\d for digits

Example - Check Python version:

[[conditions]]
file_contains = "python.*3\\.(9|10|11)"
file = "pyproject.toml"

Example - Check dependencies:

[[conditions]]
file_contains = "fastapi.*==.*0\\."
file = "requirements.txt"

file_doesnt_contain + file

Check if a file does NOT contain a pattern.

[[conditions]]
file_doesnt_contain = "python.*2\\."
file = "setup.py"

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:

  1. โœ… Project must have setup.cfg (old configuration)
  2. โœ… 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:

  1. โœ… Only extract if we haven't already extracted a compose file
  2. โœ… 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:

  1. Try compose.yaml (modern name)
  2. If that fails, try compose.yml
  3. If that fails, try docker-compose.yaml
  4. If that fails, try docker-compose.yml
  5. 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:

  1. Filters for broad technology targeting
  2. Remote conditions for file-based applicability
  3. 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:

  1. Filter reduces 1000 projects โ†’ 50 Python 3.9 projects with failing builds
  2. Remote conditions reduce 50 projects โ†’ 30 projects needing migration (have setup.cfg, no pyproject.toml)
  3. Action conditions skip Docker-related actions for non-Docker projects

See Also