Skip to content

Enterprise API Automation

Lesson 8 of 9Advanced16 min readGit at Scale & Enterprise Engineering · Enterprise OperationsVerified: GitHub REST and GraphQL API documentation, September 2026

Everything in this pillar becomes an API problem at four hundred repositories.

Inventory, metadata, policy application, evidence collection, access reviews, health monitoring — none of them are things anybody does through an interface once the estate is large, and all of them are the same shape: enumerate, act, verify.

REST for writes and for simple enumeration. Predictable, well-documented, one resource per call.

GraphQL for reads that span relationships. Fetching repositories with their teams, their properties and their branch protection in one query, rather than four calls per repository.

Authenticate as a GitHub App, not as a person. An app’s token does not disappear when somebody leaves, its permissions are explicit, and its actions are attributable.

Rate limits are the binding constraint at fleet scale, and every piece of automation must handle them rather than assume it will not hit them.

Idempotency and resumption are not optional. A run over four thousand repositories will be interrupted, and it must be safe to run again.

REST costs one request per resource. Listing repositories, then their teams, then their properties, is one call plus three per repository. At four thousand repositories that is twelve thousand calls.

GraphQL fetches related data in one request. The same picture might be a few hundred paginated queries. For read-heavy fleet operations this is the difference between a job that takes twenty minutes and one that takes four hours.

But GraphQL has its own cost model. Query complexity is scored, and a deeply nested query over many nodes is expensive even though it is one request. Fetching less per page is sometimes faster overall.

REST is better for writes. Most mutations are simple, well documented, and available in REST when they may not be in GraphQL.

The practical pattern for fleet work: GraphQL to build the picture, REST to act on it. Read broadly and cheaply, then write specifically.

And prefer the search and listing endpoints over enumerating everything when you can express the filter server-side. Fetching only what matches beats fetching everything and filtering locally, on every axis that matters.

They will be hit. Any assumption otherwise is a bug waiting to appear during a large run.

Handle them properly:

Read the rate limit headers on every response, and slow down before exhausting the budget rather than after.

Respect retry-after. When the API tells you to wait, wait. Retrying immediately makes the situation worse and can extend the restriction.

Back off exponentially on errors, with jitter. Several parallel workers all retrying at the same interval produce a thundering herd.

Limit concurrency. Parallelism helps until it triggers abuse detection, and the threshold is lower than most people assume. Modest concurrency with good backoff outperforms aggressive concurrency that gets throttled.

Spread expensive work over time. A nightly job with a twelve-hour window does not need to complete in ten minutes.

Different endpoints have different budgets, and search in particular is more constrained than general reads. Automation that hammers search will be limited long before one making equivalent listing calls.

GitHub App, essentially always. Explicit permissions, per-repository installation, short-lived installation tokens, attributable actions, and no dependency on anybody’s employment.

Never a personal access token for fleet automation. It carries a person’s access, breaks when they leave, misattributes every action to them, and is subject to identity controls the automation cannot satisfy. See machine identities.

Scope the installation. An app installed on all repositories has fleet-wide access at its permission level. If the automation reads, install it with read permissions. If it writes to a subset, install it on that subset.

Store the private key in a secret manager, and rotate it.

Use the narrowest permission that works. An automation that sets custom properties needs the property permission, not administration. Reviewing an app’s permission list against what it actually calls is a productive exercise and usually finds excess.

The properties that separate automation you can run confidently from automation you run and then check nervously.

Idempotent. Running it twice produces the same result as running it once. Setting a property to a value is idempotent; appending a line to a file is not.

Resumable. A run over four thousand repositories will be interrupted — by a rate limit, a timeout, a network failure, or somebody stopping it. It must be safe to run again, and it should not redo completed work.

Dry-run by default. The first mode should report what it would do. A fleet operation that acts before anybody has read the plan is how an estate gets damaged.

Logged per item. What was attempted, what happened, and why it was skipped. When a run over four thousand repositories reports “3,847 succeeded”, the interesting question is the other 153.

Bounded. A maximum number of items per run, so a bug affects fifty repositories rather than four thousand.

Reversible where possible. Know how to undo before you do.

Tested on one. Then ten. Then a hundred. Then the estate. Every stage finds something the previous one did not.

Polling answers “what is the state now”. Webhooks answer “what just changed”, and a mature fleet operation uses both.

Event-driven is cheaper and faster. A repository creation webhook that sets properties immediately beats a nightly sweep, on both counts.

And it closes gaps. A repository created and deleted between sweeps is invisible to polling and visible to webhooks.

But webhooks are not reliable enough alone. Deliveries fail, endpoints go down, and events are missed. A webhook-driven system with no periodic reconciliation will drift, and the drift is silent.

The pattern that works: webhooks for immediacy, a periodic sweep as a backstop that catches whatever the webhooks missed. The sweep’s finding count is also a health metric for the webhook path — a rising number means deliveries are failing.

Organisation and enterprise webhooks cover events across many repositories from one configuration, which is what makes this tractable at fleet scale. A webhook per repository is the unmaintainable version.

Verify the signature. A webhook endpoint that acts on unverified payloads acts on anything anybody sends it, and the endpoint is usually reachable from the internet.

And handle redelivery. Webhook delivery can repeat, so handlers must be idempotent — the same requirement as everywhere else in this article. See webhooks.

Every listing endpoint pages, and the most common fleet-automation bug is reading the first page and stopping.

Follow the links. REST provides link headers; GraphQL provides cursors. Use them rather than constructing page numbers.

Do not assume stability. The underlying data changes while you paginate. An item can be missed or seen twice, and for an inventory that is usually acceptable — for anything that must be exact, take that into account.

Record where you stopped. A resumable job needs to know its position, and a cursor is that position.

Watch the total. If a job that normally processes four thousand repositories processes four hundred, something truncated silently and the result looks like a successful run.

A choice with consequences for reliability, credentials and visibility.

GitHub Actions on a schedule. The obvious home: no infrastructure, the credential can be an app installation token generated in the workflow, and the run history is visible. Constrained by job timeout, which matters for a run over thousands of repositories.

A scheduled job on your own infrastructure. More control, longer runs, and an operational burden — a machine, a scheduler, monitoring, and a credential to store.

A serverless function. Good for event-driven work — react to a repository creation webhook by setting properties — and awkward for long enumerations.

Event-driven beats scheduled where possible. Setting a repository’s metadata when it is created is better than a nightly sweep that finds repositories created since yesterday. The sweep is still worth having as a backstop.

Whichever you choose, monitor it. The failure mode for scheduled automation is that it silently stops running, and nobody notices until somebody asks for data that has not been collected for three months. An alert on “this job has not completed successfully in 48 hours” is the single most valuable piece of monitoring around fleet automation.

And log where somebody will look. A run history nobody can find is a run history nobody checks.

Fleet automation fails on the repositories that do not match your assumptions, and there are always some.

Archived repositories reject writes. A bulk operation must skip them or handle the error, and “skip archived” should usually be the default.

Empty repositories have no default branch, and anything assuming one fails.

Repositories with unusual default branch names. Do not assume; read it.

Forks may need different treatment, or exclusion entirely.

Repositories the app is not installed on are invisible to it, which is correct and means your enumeration is scoped to the installation rather than to the organisation. Know which you are getting.

Repositories with protections that block the automation’s write. A bulk pull request against a repository requiring signed commits will fail if the automation cannot sign.

Very large repositories time out on operations that work fine elsewhere.

Repositories in the middle of something — a migration, a transfer — may behave unexpectedly.

The design conclusion: expect a percentage of failures on every fleet run, log them individually with reasons, and treat the failure list as the output that matters. A run reporting 96% success is normal; the 4% is where the interesting information is.

The fleet tasks that recur, and their shape.

Inventory. Enumerate everything, store it, diff against the previous snapshot. The foundation for reviews, evidence and health monitoring.

Metadata backfill. Derive a custom property from a source of truth and set it where unset. Report what could not be derived rather than guessing. See custom properties.

Configuration reconciliation. Compare actual settings against intended ones and report differences. Report, do not silently correct — a job that overwrites hides the fact that somebody changed it deliberately.

Bulk pull requests. Applying a change across many repositories. The hardest category, because it involves writes to code and because four hundred pull requests need four hundred reviews. See standardised CI/CD.

Evidence collection. Periodic export of membership, permissions and configuration. See compliance evidence.

Health checks. Repository sizes, stale branches, missing metadata, unowned repositories.

The hardest fleet operation, because it writes to code and because the bottleneck is human.

The mechanical part is straightforward: clone or fetch, apply a change, commit, push a branch, open a pull request. Four hundred times.

The bottleneck is review. Four hundred pull requests need four hundred approvals from four hundred different teams, and most of them will sit.

Which shapes the design:

Make the change trivially reviewable. A one-line diff with a clear title and a body explaining why. If a reviewer has to think, they will defer.

Batch by team, not by repository. A team receiving one pull request across their eight repositories engages; a team receiving eight separate ones ignores them.

Explain in the pull request body, not in a separate announcement. The reviewer reads what is in front of them.

Give a deadline and a consequence. “Unmerged by the 30th and we will merge on your behalf” works, provided you actually have the permission and the mandate. Say it in advance.

Handle the failures individually. Repositories where the change does not apply cleanly are a smaller set, and they need a person.

Rate-limit the opening. Four hundred pull requests opened in five minutes is a notification storm that teaches everybody to mute the bot.

Track merge rate. It is the metric for whether the campaign worked, and a stalled rate at week two means the approach needs changing rather than more time.

And consider whether a pull request is the right mechanism at all. For configuration that could live centrally — a reusable workflow, a ruleset, an organisation setting — changing it centrally is one action instead of four hundred. See standardised CI/CD.

Because the blast radius is the whole estate, the testing discipline is different from ordinary software.

A sandbox organisation. Repositories you can damage, in configurations matching the real ones — archived, empty, forked, protected. This is the single most valuable investment for anybody doing fleet work.

Dry-run against production. Read everything, plan everything, write nothing. Then read the plan.

A canary set. Five real repositories, chosen to include one unusual one, run for real. Check the results by hand.

Then a bounded expansion. Fifty, then five hundred, then the rest, checking between each.

Keep the bound configurable, so an unexpected result is a small cleanup rather than a large one.

Verify afterwards independently. Re-read the state and confirm it matches what the automation reported doing. Automation that reports success and did nothing is a real failure mode, particularly where an API returns success for a no-op.

And keep the before state. A snapshot taken immediately before a fleet write is what makes an undo possible.

Personal access tokens for fleet automation. Breaks on departure, misattributes, over-scoped.

Assuming rate limits will not be hit. They will.

Retrying immediately on a rate limit. Makes it worse.

Reading only the first page. The most common silent bug.

No dry-run mode. Acting before anybody has read the plan.

Not resumable. An interrupted run over thousands of repositories is unrecoverable without it.

Silently correcting drift. Hides deliberate changes and destroys the reason for them.

Unbounded runs. A bug reaches the whole estate.

No per-item logging. “3,847 succeeded” without the other 153 is not a result.

Aggressive concurrency. Triggers throttling and consumes a shared budget.

Fleet automation produces data, and where that data lives determines what you can do with it.

A flat file per run is enough to start. JSON or CSV, timestamped, in object storage. Cheap, simple, and it supports the diff-against-previous pattern that most fleet reporting needs.

Keep every snapshot. They are small, and the historical series answers questions current state cannot: when did this repository become public, how has the unowned count trended, what did access look like in March.

A database when the queries get complex. Once you are joining repositories to teams to properties to permissions, a query engine beats a script. This is a natural second step and not a necessary first one.

Normalise the shape. A consistent record structure across collectors means reports can join them. Ad-hoc shapes per script produce data nobody can combine.

Include collection metadata. When it ran, what it covered, what failed. A snapshot with no record of its own completeness is a snapshot you cannot trust.

Treat it as containing personal data. Membership and permission records name individuals and describe their access. Retention, access control and deletion obligations apply, and that is a conversation with whoever owns data protection.

And make it queryable by more than its author. The value of fleet data is that other people can answer their own questions with it. A dataset only one engineer knows how to read is a dependency on that engineer.

Before writing anything, gh covers a surprising amount of fleet work.

It handles authentication, pagination and rate limiting for you, which are three of the five hard parts.

gh api reaches any endpoint, with automatic pagination, which makes ad-hoc queries fast to write.

It scripts well. Piped into standard text tools, it answers most inventory questions without a program.

Which makes it the right first tool for exploration and for one-off operations. Find out what the data looks like with the CLI, and write a program only when the operation becomes recurring or needs the safety properties above.

Its limits are the ones that matter for production automation: no dry-run semantics of its own, no resumption, no per-item logging, and authentication that is typically a person’s. A shell loop over four thousand repositories with gh is a fine way to explore and a poor way to operate.

The progression is: explore with the CLI, prototype as a script, and promote to a proper job with the safety properties when it becomes something you rely on. See scripting with the GitHub CLI.

Fleet automation becomes infrastructure, and infrastructure needs an owner.

Written by one person is the normal starting point and the normal failure. A collection of scripts on one engineer’s machine, authenticating as them, understood by them alone, is a dependency on their continued employment.

Put it in a repository. Reviewed, versioned, and readable by the team. This is the minimum bar and it is frequently not met, because fleet scripts start as one-offs.

Give it the same treatment as any other service. Tests, a runbook, monitoring, an on-call answer for when it fails.

Document what each job does and what it touches. A future engineer looking at a job that writes to four thousand repositories needs to know what it changes before they run it.

Review the permissions annually. Automation accumulates permissions the way everything else does.

And have a kill switch. A way to stop every fleet job quickly, known to more than one person. The scenario is a job doing something wrong at scale, and the response time matters.

Treat a fleet write capability as privileged. Whoever can run these jobs can change four thousand repositories, and that is a level of access worth being deliberate about — the same reasoning applied to any other powerful capability in this pillar.

Fleet automation is enumerate, act, verify — and the hard parts are all in the failure cases. It will be interrupted, it will hit limits, and it will encounter repositories that do not match your assumptions. Automation that is idempotent, resumable, bounded and dry-runnable can be run confidently; automation that is not will eventually do something across four thousand repositories that somebody has to undo by hand.

  • GraphQL is better for reads spanning relationships; REST is better for writes and simple enumeration
  • Query complexity is GraphQL’s cost model, so one large query is not automatically cheaper
  • Authenticate as a GitHub App with a narrowly scoped installation, never as a person
  • Rate limits are the binding constraint and must be handled with header awareness, retry-after and backoff
  • Modest concurrency with good backoff outperforms aggressive concurrency that gets throttled
  • Automation must be idempotent, resumable, bounded, dry-runnable and logged per item
  • Pagination must follow the provided links or cursors, and stopping at page one is the classic silent bug
  • Reconciliation should report drift rather than silently correcting it
  • Test on one, then ten, then a hundred, then the estate

Use an organisation you have API access to, or a sandbox organisation. Every step is read-only or dry-run — no production credentials, and nothing writes to a repository you did not create for this.

  1. Write a script that lists every repository with its visibility and size. Handle pagination.

  2. Run it and count the results. Compare against the organisation’s stated repository count. Predict: do they match?

  3. Rewrite the read as a GraphQL query fetching repositories with their custom properties. Compare the request count.

  4. Add rate limit handling: read the headers, slow down before exhaustion, respect retry-after.

  5. Add a dry-run mode to a script that would set a property, and run it.

  6. Interrupt a long run halfway. Predict: can you resume without redoing everything?

  7. Check what identity your automation authenticates as. If it is a personal token, design the app replacement.

  8. Review that identity’s permissions against what your script actually calls.

Engineering Team Onboarding SystemA 30-day Git and GitHub programme with standards templates, assessments and governance checklists.