← Back 05

Architectural Blueprint: Programmatic CI/CD via Dagger.io

May 2026 · Part 5 of a 5-part series on migrating CI/CD from GitHub to Codeberg

Root Cause Analysis

The 21 migration gotchas documented in Part 3 share a single structural dependency: the limitations of declarative YAML for complex pipeline orchestration.

Attempting to implement logic-heavy workflows without native support for workflow_run or workflow_call leads to severe configuration bloat. The current production ci.yml spans 524 lines across 8 jobs, with highly duplicated Buildx cache declarations, fragile shell escaping rules, and silent execution failures (e.g., hyphenated job-ID parsing bugs).

The technical debt cannot be resolved by further YAML refactoring. The pipeline logic must be migrated to an imperative execution model.

Technical Overview: Dagger.io SDK

Dagger.io replaces YAML-based steps with an application-level SDK. Pipelines are executed as code (Python, Go, or TypeScript), converting the runner’s configuration file into a simple bootstrap script that calls the underlying program:

dagger run python ci/main.py

Comparison Matrix

CapabilityForgejo YAML (Current)Dagger Python (Proposed)
Pipeline Definitions8 YAML jobs via standard DAG configurationSingle bootstrap job executing a Python script
Step ExecutionInline POSIX shell scripts inside YAML stringsStructured Python functions using the Dagger SDK
Caching LayerForgejo custom cache actions (uv/bun)Native Dagger engine cache_volume() API
ConcurrencyDeclarative needs dependency mappingAsynchronous scheduling via anyio.create_task_group()
Tool AvailabilityStep-by-step runner environment setupExecution inside pre-configured Dagger containers
Secret InjectionHeredoc interpolation via environment blockswith_new_file() SDK method (bypasses the shell)
Validation LoopRemote runtime observation onlyLocal compilation, debugging, and unit testing

Pipeline Topology

The refactored runtime logic consists of approximately 370 lines of asynchronous Python execution split across three sequential execution blocks.

Key Engineering Decisions

Runtime Engine Language Selection

The Python SDK was selected to align with the core backend services (Python 3.13). This allows the engineering team to reuse existing asynchronous patterns (anyio) already present in our application components (such as pydantic-ai) without adding a new language runtime to the tech stack.

Secret Isolation via File Injection

To completely eliminate the double-base64 encoding issue exposed during kubectl create secret calls, the pipeline completely removes shell-based interpolation. Codeberg repository secrets are loaded into the initial execution context as environment variables (os.environ), and injected directly into target deployment containers as filesystem inputs via Dagger’s with_new_file() method.

Shared DinD Daemon Socket Architecture

By default, the Dagger CLI provisions an independent container running Buildkit to manage execution steps. To prevent nested virtualization performance degradation (Docker-in-Docker-in-Docker), the execution environment is explicitly configured to attach to the runner’s underlying daemon socket via environment configuration:

DOCKER_HOST=unix:///var/run/docker.sock

This binds Dagger directly to the existing GKE node container daemon, reducing resource consumption and containing the network attack surface.

Deterministic Build Verification

Infrastructure code like configuration compilers, environment file generation scripts, and asset argument maps are written as isolated Python utilities. They are verified locally using standard unit tests, while integration routines requiring an active Dagger engine are partitioned under @pytest.mark.e2e blocks to optimize local development feedback loops.

Implementation & Migration Strategy

The transition plan uses a 5-phase canary model designed to keep rollback costs near zero. If a failure occurs during any validation step, reverting to the legacy pipeline requires a single Git command: git checkout HEAD~1 -- ci.yml.

[Phase A: Implement] ──> [Phase B: Validate] ──> [Phase C: Coexistence] ──> [Phase D: Cutover] ──> [Phase E: Cleanup]

Infrastructure Configuration Management

    [Legacy Context]                            [Target Context]
   524 Lines of YAML                          ~100 Lines of Bootstrap YAML
   (Direct Orchestration)                        (Spins up Dagger CLI)
                                                         │
                                                         ▼
                                                370 Lines of Python
                                               (Programmatic Pipeline)

The data below summarizes the structural components before and after the architecture shift:

Risk Assessment & Mitigations

1. Dagger Socket Connection Refusal

2. Cache Performance Regression vs. Native Buildx

3. Pipeline Runtime Transparency Breakdown

4. SDK Version Drift

Operational Impact

This re-architecture trades standard declarative configuration files for an execution model that values unit-testability, predictable secret parsing, and maintainable exception handling. Programmatic pipeline code can be refactored, shared, and version-controlled with the same degree of rigor applied to standard enterprise software assets.

← Results & Costs