feat: Enterprise-level runners - #5096
Conversation
669630b to
1313567
Compare
|
What is the rate limit for this type of pat? Maybe allow multiple pat and set some round robin logic. There is an open pr to support multiple apps for this reason. |
|
@edersonbrilhante as usual, 5000 requests/hour. For this reason, in this implementation When a function creates |
36fd927 to
8b9e1c7
Compare
|
What's the reason for supporting PAT's and not just enterprise github app's? |
|
@Brend-Smits unfortunately, GitHub App on enterprise level does not allow to manage runners.
|
Okay thanks! I will try to arrange a test enterprise to be able to test this and get back to you 👍🏼 |
5ee5f67 to
8113d6c
Compare
|
|
||
| enable_organization_runners = true | ||
| runner_extra_labels = ["default", "example"] | ||
| runner_registration_level = "org" |
There was a problem hiding this comment.
I like this change, but maybe instead of completely removing the 'enable_organization_runners' and making this a breaking change, instead we can just deprecate that option and move people to this new option instead?
In the mean time we can keep the old variable working. If that is used, then it takes priority or something?
There was a problem hiding this comment.
Makes sense. Deprecated enable_organization_runners instead of breaking it: it still works, and if set to true, it takes priority over the new runner_registration_level for backwards compatibility.
edersonbrilhante
left a comment
There was a problem hiding this comment.
@dmitrykiselev27 can you fix the conflicts?
6b14465 to
f473616
Compare
|
@edersonbrilhante rebased and fixed the conflicts |
Can you fix please? |
9f3b3cd to
ba83816
Compare
|
@edersonbrilhante fixed |
|
I think is good. But I don't have enterprise env to test. Have you tested after fixing the conflicts? |
ba83816 to
991850d
Compare
5310e90 to
5a7fb68
Compare
|
You moved the policy from json to data. This is a not problem for me, but it created a mixed approach when compared with other modules. I personally prefer data, but I think this could be a following PR. |
|
Could we move this nonsensitive() change to a separate PR? It appears unrelated to enterprise runner support and adds noise to an already large change. If it is required for this feature, please document the specific Terraform error or sensitive-value propagation it fixes. |
|
@dmitrykiselev27 Can you fix the conflicts? Heads up, I am working to refactor the module to introduce plugin layout. Ref: #5234 |
739c2e2 to
b0647c3
Compare
b246aa5 to
a16eb9e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated no new comments.
Suppressed comments (6)
modules/multi-runner/variables.tf:45
- enterprise_slug is documented as required when runner_registration_level is "enterprise", but the module does not currently enforce that relationship. Since multi_runner_config can now opt into enterprise registration, validate that enterprise_slug is set whenever any runner config resolves to enterprise.
modules/multi-runner/variables.tf:71 - enterprise_pat is documented as required when runner_registration_level is "enterprise", but the module does not enforce that it’s set when any runner opts into enterprise registration. Without this, enterprise deployments can plan/apply successfully but fail at runtime when the lambdas try to create an enterprise PAT client.
lambdas/functions/control-plane/src/scale-runners/runner-config.ts:13 - resolveRunnerType() only considers RUNNER_REGISTRATION_LEVEL and defaults to Repo when it’s unset. The PR description says there’s a backward-compatible fallback to the legacy ENABLE_ORGANIZATION_RUNNERS env var, but the current implementation ignores it entirely (and production code no longer reads ENABLE_ORGANIZATION_RUNNERS), which can change behavior for existing deployments that haven’t been updated to set RUNNER_REGISTRATION_LEVEL.
export function resolveRunnerType(): RunnerType {
const registrationLevel = process.env.RUNNER_REGISTRATION_LEVEL;
if (registrationLevel) {
switch (registrationLevel) {
case 'enterprise':
lambdas/functions/control-plane/src/scale-runners/job-retry.ts:47
- In enterprise mode, runnerOwner falls back to an empty string when ENTERPRISE_SLUG is missing. That will later be used as the GitHub owner and logged/metric-tagged, and will likely cause confusing API calls (and errors) compared to scale-up/pool which explicitly fail fast when ENTERPRISE_SLUG is missing.
const runnerType = resolveRunnerType();
const enableOrgLevel = runnerType !== 'Repo';
const runnerOwner =
runnerType === 'Enterprise'
? (process.env.ENTERPRISE_SLUG ?? '')
modules/multi-runner/variables.tf:33
- The multi-runner module now allows omitting github_app.id/key_base64 entirely (it only validates webhook_secret). If any runner in multi_runner_config registers at repo/org level, missing id/key_base64 will lead to runtime failures in the lambdas. Consider validating that id+key_base64 are provided whenever any runner_registration_level resolves to repo/org (and allow them to be omitted only when all runners are enterprise-registered).
This issue also appears in the following locations of the same file:
- line 41
- line 67
lambdas/functions/control-plane/src/pool/pool.ts:51
- Use strict equality for the runnerType check to avoid implicit coercions and to match the rest of the codebase’s style (runnerType is a string union).
if (runnerType == 'Enterprise' && !enterpriseSlug) {
throw new Error('ENTERPRISE_SLUG must be set when RUNNER_REGISTRATION_LEVEL is enterprise.');
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated no new comments.
Suppressed comments (5)
lambdas/functions/control-plane/src/scale-runners/runner-config.ts:7
- The PR description says resolveRunnerType is backward-compatible via a fallback to the legacy ENABLE_ORGANIZATION_RUNNERS env var, but this implementation only checks RUNNER_REGISTRATION_LEVEL and otherwise always defaults to Repo. This can break older deployments that still set ENABLE_ORGANIZATION_RUNNERS (or tests/tools that rely on it).
/**
* Resolves the RunnerType from the RUNNER_REGISTRATION_LEVEL environment variable.
*
* Valid values: 'enterprise' | 'org' | 'repo'.
* Defaults to 'Repo' when not set.
lambdas/functions/control-plane/src/pool/pool.ts:51
- Use strict equality here;
runnerTypeis a string union, so===avoids accidental coercion and matches the rest of the codebase’s comparisons.
if (runnerType == 'Enterprise' && !enterpriseSlug) {
throw new Error('ENTERPRISE_SLUG must be set when RUNNER_REGISTRATION_LEVEL is enterprise.');
}
modules/ssm/variables.tf:69
- modules/ssm allows enterprise_pat = {} (both pat and pat_ssm null) because there is no validation. In that case, aws_ssm_parameter.enterprise_pat has count = 0 but modules/ssm/outputs.tf still references aws_ssm_parameter.enterprise_pat[0], which will fail the plan/apply.
lambdas/functions/control-plane/src/pool/pool.ts:140 - For repo-level registration, getInstallationId assumes RUNNER_OWNER is in "owner/repo" form. If it’s missing or malformed,
owner/repobecome undefined and the subsequent GitHub API call will fail with a confusing error. Please validate the format and throw a clear message.
const [owner, repo] = runnerOwner.split('/');
return (
await githubClient.apps.getRepoInstallation({
owner,
repo,
main.tf:67
- pool_runner_owner can still be null for non-enterprise stacks (e.g., default repo-level) even when pool_config is set. That results in the pool Lambda being deployed with RUNNER_OWNER="" and failing at runtime (pool.ts throws when RUNNER_OWNER is missing for org/repo levels). Add a Terraform precondition to fail fast when pool_config is non-empty and (a) registration level is repo, or (b) registration level is org and pool_runner_owner is not set.
a16eb9e to
c80394e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
lambdas/functions/control-plane/src/pool/pool.ts:49
- Use strict equality for runnerType comparisons to avoid unintended coercion and to match the rest of the codebase’s TypeScript style.
if (runnerType == 'Enterprise' && !enterpriseSlug) {
modules/multi-runner/variables.tf:33
- The github_app variable validation was relaxed to only require the webhook secret. This allows invalid org/repo configurations (missing GitHub App id/key) to pass Terraform validation and fail later at runtime. Since multi-runner defaults to repo/org registration unless explicitly set to enterprise, it should still enforce id/key unless all runner configs are enterprise.
modules/ssm/variables.tf:69 - modules/ssm.enterprise_pat lacks a validation block ensuring at least one of pat or pat_ssm is set. If a caller passes enterprise_pat = {} (or both fields null), the module will later fail (e.g., outputs.tf indexes enterprise_pat[0]). Adding validation here makes the module robust and consistent with the root module.
modules/ssm/outputs.tf:24 - enterprise_pat output unconditionally indexes aws_ssm_parameter.enterprise_pat[0] whenever enterprise_pat is non-null and pat_ssm is null. If enterprise_pat is provided but no parameter is created (e.g., invalid object / missing pat), this will cause a hard error. Add a length guard like the github_app_id/key outputs (and/or return null) to avoid index-out-of-range failures.
| export function resolveRunnerType(): RunnerType { | ||
| const registrationLevel = process.env.RUNNER_REGISTRATION_LEVEL; | ||
| if (registrationLevel) { | ||
| switch (registrationLevel) { | ||
| case 'enterprise': | ||
| return 'Enterprise'; | ||
| case 'org': | ||
| return 'Org'; | ||
| case 'repo': | ||
| return 'Repo'; | ||
| default: | ||
| throw new Error( | ||
| `Invalid RUNNER_REGISTRATION_LEVEL: '${registrationLevel}'. Must be 'enterprise', 'org', or 'repo'.`, | ||
| ); | ||
| } | ||
| } | ||
| return 'Repo'; | ||
| } |
There was a problem hiding this comment.
|
@edersonbrilhante both changes (policy and nonsensitive) were attempts to fix this error, which I'm not longer can reproduce in my test environment, so I've reverted them. |



Description
Adds enterprise-level runner registration support. Previously, runners could only be registered at the repository or organization level using GitHub App authentication. This PR introduces a third registration level — enterprise — using PAT-based authentication against the GitHub Enterprise runner management APIs.
Key changes
Terraform
runner_registration_levelvariable ("repo" | "org" | "enterprise") replaces the booleanenable_organization_runnersenterprise_slugandenterprise_patvariables for enterprise configuration. Supports comma-separated multiple PATs for rate limit distribution.github_appno longer requireskey_base64oridwhenrunner_registration_level = "enterprise"— onlywebhook_secretis needed. No GitHub App creation required for enterprise runners.Lambda functions
client.request()since Octokit's typed helpers don't cover enterprise endpointscreateEnterprisePATClient()inauth.tsreads PAT from SSM and randomly selects one token from a comma-separated list per invocationresolveRunnerType()helper readsRUNNER_REGISTRATION_LEVELenv var with backward-compatible fallback to the legacyENABLE_ORGANIZATION_RUNNERSTest Plan
manage_runners:enterprisescope.Workflow jobsonlyRelated Issues