How to Mock a GitHub Actions Workflow in Python

Build a dataclass-based model of a GitHub Actions workflow and simulate its execution to validate steps and outputs before deployment.

Easy Python 3.9+ Aug 9, 2026 Production deployment patterns 13 views 0 copies

Python code

81 lines
Python 3.9+
import json
from dataclasses import dataclass, asdict
from typing import List, Dict, Any


@dataclass
class Step:
    name: str
    run: str


@dataclass
class Job:
    name: str
    steps: List[Step]
    runs_on: str = "ubuntu-latest"


@dataclass
class Workflow:
    name: str
    jobs: List[Job]

    def to_github_actions_yaml(self) -> Dict[str, Any]:
        """Generate a GitHub Actions workflow dictionary (simulated)."""
        workflow_dict = {
            "name": self.name,
            "on": ["push"],
            "jobs": {},
        }
        for job in self.jobs:
            job_dict = {
                "runs-on": job.runs_on,
                "steps": [
                    {"name": step.name, "run": step.run}
                    for step in job.steps
                ],
            }
            workflow_dict["jobs"][job.name] = job_dict
        return workflow_dict

    def execute(self) -> Dict[str, List[str]]:
        """Mock-execute each step, collecting printed outputs."""
        results: Dict[str, List[str]] = {}
        for job in self.jobs:
            outputs = []
            for step in job.steps:
                outputs.append(f"Running: {step.name}")
                outputs.append(f"Command: {step.run}")
                outputs.append(f"Result: SUCCESS")
            results[job.name] = outputs
        return results


if __name__ == "__main__":
    workflow = Workflow(
        name="CI Pipeline",
        jobs=[
            Job(
                name="test",
                steps=[
                    Step(name="Install deps", run="pip install -r requirements.txt"),
                    Step(name="Run tests", run="pytest"),
                ],
            ),
            Job(
                name="build",
                steps=[
                    Step(name="Compile", run="python setup.py build"),
                    Step(name="Package", run="python setup.py sdist"),
                ],
            ),
        ],
    )

    generated_yaml = workflow.to_github_actions_yaml()
    print("Generated workflow (JSON):")
    print(json.dumps(generated_yaml, indent=2))

    print("\nExecuting workflow simulation:")
    print(json.dumps(workflow.execute(), indent=2, default=str))

Output

stdout
Generated workflow (JSON):
{
  "name": "CI Pipeline",
  "on": [
    "push"
  ],
  "jobs": {
    "test": {
      "runs-on": "ubuntu-latest",
      "steps": [
        {
          "name": "Install deps",
          "run": "pip install -r requirements.txt"
        },
        {
          "name": "Run tests",
          "run": "pytest"
        }
      ]
    },
    "build": {
      "runs-on": "ubuntu-latest",
      "steps": [
        {
          "name": "Compile",
          "run": "python setup.py build"
        },
        {
          "name": "Package",
          "run": "python setup.py sdist"
        }
      ]
    }
  }
}

Executing workflow simulation:
{
  "test": [
    "Running: Install deps",
    "Command: pip install -r requirements.txt",
    "Result: SUCCESS",
    "Running: Run tests",
    "Command: pytest",
    "Result: SUCCESS"
  ],
  "build": [
    "Running: Compile",
    "Command: python setup.py build",
    "Result: SUCCESS",
    "Running: Package",
    "Command: python setup.py sdist",
    "Result: SUCCESS"
  ]
}

How it works

Using dataclasses gives a clean, type‑safe structure for representing workflow entities like Workflow, Job, and Step. The to_github_actions_yaml method builds a standard GitHub Actions dictionary that mirrors what you'd write in a YAML file, making it easy to inspect or convert to actual YAML later. The execute method simulates running each step by printing the step name, the command, and a placeholder status — useful for validating orchestration logic or generating logs without triggering real side effects. This pattern works because it separates the workflow definition (data) from execution (behavior), allowing easy testing and reuse.

Common mistakes

  • Using `json.dumps` on dataclasses without `default=str` causes a TypeError for non‑serializable objects.
  • Forgetting to set `runs_on` for jobs that need a different runner (e.g., `windows-latest`).
  • Not simulating failure paths — always make the mock return SUCCESS, even when a real step might fail.
  • Hardcoding workflow names or job names, making the mock inflexible for multiple environments.

Variations

  1. Use `yaml.safe_dump` from PyYAML to generate actual YAML text instead of a dictionary.
  2. Add a `fail_after` parameter to the `execute` method to randomly fail a step, testing error handling.

Real-world use cases

  • Pre‑deployment validation of CI pipelines: verify step order and commands without hitting real build servers.
  • Documentation and training: generate readable workflow structures for onboarding or audits.
  • Testing automation that reacts to workflow events, such as a service that parses CI status messages.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Production deployment patterns

Related tutorials and quizzes for this topic.