Unifying CI Jobs with Run Leases, Per-Run RBAC, and Git-Based Change Control
Back
Infrastructure7 min read

Unifying CI Jobs with Run Leases, Per-Run RBAC, and Git-Based Change Control

By Taylor

Move CI operations into a job control plane using run leases, per-run RBAC, and Git-reviewed job definitions for safer runs.

Why “just another CI job” stops scaling

Most teams start with CI as a convenient place to run anything that needs compute: build steps, migrations, backfills, one-off scripts, “temporary” admin fixes, or production diagnostics. Over time, the CI system becomes an accidental job platform. That’s when the pain shows up:

  • Jobs are hard to reason about because they mix build/test concerns with operational tasks.
  • Credentials are shared and long-lived, so the blast radius of “one bad run” is larger than anyone admits.
  • There’s no consistent model for approvals, change control, and auditability across repos and teams.
  • Reruns and retries create duplicate side effects (double migrations, double invoices, double notifications).

A unified internal job control plane tackles the problem at the right layer: not “where do we run code,” but “how do we safely request, authorize, lease, execute, and audit a run.” The concepts that make this practical are run leases, per-run RBAC, and Git-based change control.

What a job control plane actually is

Think of a job control plane as the API and policy layer in front of execution. Your executors (Kubernetes jobs, containers, serverless tasks, workers) can remain heterogeneous. The control plane standardizes:

  • Run identity: every execution is a first-class object with a unique ID, inputs, and metadata.
  • Authorization: the decision of who can run what, with which parameters, against which resources.
  • Change control: how job definitions and permissions evolve (reviewed, versioned, rolled back).
  • Observability: logs, status, timings, and audit events in one place.

The “migration” is usually not a forklift. It’s a progressive replacement of ad-hoc CI steps with job specs that are run through a control plane and executed by a consistent worker layer.

Run leases prevent duplicate side effects

A run lease is a time-bounded claim that a specific job run is allowed to proceed. It’s a guardrail against race conditions, retries, and concurrent executions that would otherwise create non-idempotent outcomes.

What a lease gives you

  • Mutual exclusion: only one actor can hold the lease for a given run key (for example: “migrate-prod-2026-08-14”).
  • Expiration: if a worker dies, the lease expires and the run can be safely re-acquired.
  • Renewal: long-running jobs can renew the lease on a heartbeat.
  • Explicit ownership: the lease holder is recorded for auditability.

Designing lease keys

The key is where most teams go wrong. If it’s too broad, you block legitimate parallelism. If it’s too narrow, you still get duplicates. Practical patterns:

  • Resource-scoped: “backfill:account:12345” prevents two backfills on the same account.
  • Environment-scoped: “schema-migration:prod” prevents overlapping prod migrations.
  • Time-window: “rollup:2026-08-14” ensures daily aggregation runs once.

A control plane can also support lease groups (one job holds multiple leases) when a run touches several shared resources.

Per-run RBAC makes ephemeral permissions the default

Classic CI tends to rely on one of two models: shared “CI credentials” or a sprawling matrix of secrets injected by environment. Neither is great. Per-run RBAC flips the model: each run gets an explicit permission boundary derived from a policy decision at request time.

How per-run RBAC works

  • Job definition declares intent: which connectors, tables, buckets, queues, or APIs it needs.
  • Run request includes context: who triggered it, from where (branch, repo, environment), and with which parameters.
  • Policy engine evaluates: whether that actor is allowed to perform that run, with those parameters, right now.
  • Ephemeral credentials are minted: scoped tokens or short-lived secrets bound to that run ID.

This is where a unified control plane shines: you can standardize the “permission envelope” even if execution happens across different worker pools.

What to authorize beyond “can run job”

Per-run RBAC is most valuable when it goes deeper than a boolean:

  • Parameter constraints: allow running “backfill” only for accounts owned by a team.
  • Environment constraints: allow prod runs only from protected branches and approved actors.
  • Resource constraints: allow read-only access by default; elevate writes only for specific jobs.
  • Time constraints: allow heavy jobs only within maintenance windows.

If you’ve ever needed a clean way to do safe, reviewed operational changes, the same governance pattern applies to job execution. Teams that already practice Git-reviewed operations will recognize the symmetry with safe self-service database changes with Git-reviewed migration jobs.

Git-based change control keeps jobs auditable and reviewable

Once CI jobs become “production operations,” treating them like code stops being optional. Git-based change control for job definitions is about making job behavior and permissions reviewable artifacts.

What belongs in Git

  • Job definitions: commands/scripts, default parameters, scheduling, concurrency limits.
  • Policy mappings: which roles can request which jobs, with which constraints.
  • Connector declarations: which external systems a job can touch.
  • Runbooks: plain-language notes on impact, rollback, and expected runtime.

Git-based control also gives you safe rollback: reverting a commit should revert both the implementation and the permissions boundary.

Branch-to-environment workflows

A practical pattern is to map Git branches to job “release channels”:

  • Main: production job definitions (protected).
  • Release: staging or pre-prod.
  • Feature branches: ephemeral test environments or sandboxes.

This enables a familiar promotion model, but with a crucial upgrade: permissions and execution policy are promoted alongside code.

Putting it together in a migration plan

You don’t need to migrate everything at once. A typical sequence looks like this:

1) Inventory and classify existing CI jobs

  • Build/test: keep in CI.
  • Operational: backfills, migrations, syncs, admin tasks.
  • Data movement: ETL/ELT steps, scheduled transforms.

Operational and data movement jobs are the best initial candidates for the control plane.

2) Add run identity and logging first

Even before leases and RBAC, standardize run IDs, structured logs, and status reporting. This makes later policy work tractable.

3) Introduce leases for non-idempotent jobs

Start with the jobs that hurt the most when duplicated: migrations, billing tasks, irreversible writes. Add lease keys and renewal logic.

4) Move to per-run RBAC with short-lived credentials

Replace long-lived CI secrets with run-scoped tokens. Aim for default read-only access and explicit elevation when needed.

5) Shift job specs and policies into Git

Make the control plane consume versioned definitions, with diffs and review. Tie approvals to protected branches and CODEOWNERS-like controls.

Where Windmill fits naturally

A job control plane needs three things to be useful day to day: real code execution, strong permissioning, and a Git-friendly workflow. Windmill (see windmill.dev) is a pragmatic reference point here because it’s code-first (many languages), designed around workflows and execution, and includes granular RBAC, auditability, and Git-based collaboration patterns that map well to run-scoped authorization and change control.

In practice, teams use this kind of platform to pull operational jobs out of CI, model them as scripts and DAG workflows, run them on scalable worker groups, and make every run observable and attributable—without reinventing a bespoke orchestration layer.

Common failure modes to avoid

  • Leases without clear semantics: define what “duplicate” means per job, then pick a lease key accordingly.
  • RBAC that only checks the triggerer: include parameters, environment, and downstream resources in the decision.
  • Git control without runtime guarantees: review is not enough; enforce policy at run time and record audit events.
  • Keeping “temporary” CI secrets: the migration is incomplete until long-lived credentials are gone.

The goal is not to make CI worse; it’s to stop using CI as the place where production operations quietly happen. A unified control plane makes those operations explicit, policy-driven, and safe to scale across teams.

Frequently Asked Questions

How do run leases reduce risk when migrating CI jobs to Windmill?

Run leases give each Windmill run a time-bounded claim, preventing duplicate executions of non-idempotent jobs (like migrations) and enabling safe retries after worker failure.

What does per-run RBAC mean in a Windmill-based job control plane?

Per-run RBAC means Windmill authorizes every execution with a run-specific permission boundary—often paired with short-lived credentials—so a job run only gets the exact access it needs.

Should build-and-test pipelines move into Windmill too?

Usually no. Keep build/test in CI, and migrate operational jobs (backfills, migrations, admin tasks, data syncs) into Windmill where leases, RBAC, and audit logs are stronger.

How do you implement Git-based change control for jobs in Windmill?

Store job definitions, workflow graphs, and permission/policy mappings in Git, require reviews for protected environments, and promote changes through branches so behavior and access evolve together.

What’s the quickest first step to unify CI jobs with Windmill without a full rewrite?

Start by routing a small set of operational CI jobs through Windmill as scripts with consistent run IDs and centralized logs, then add run leases and per-run RBAC once the execution path is stable.

Continue Reading