Documentation
¶
Overview ¶
Package codebuild runs one AWS CodeBuild build per job.
IT DOES NOT USE CODEBUILD'S OWN GITHUB ACTIONS RUNNER INTEGRATION, and that is the first thing to know about it. That feature is webhook-only — a "Runner project" needs a WORKFLOW_JOB_QUEUED webhook and a `codebuild-<project>-${{ github.run_id }}-…` label, and CodeBuild fetches the runner registration token itself during DOWNLOAD_SOURCE and overrides the buildspec — so AWS would own job detection, registration and scheduling, and billet's scale-set listener, capacity ledger, ordered fallback, custody and stable-label contract would all be bypassed. Checked against the API reference rather than inferred: there is no API-invoked form of it. So this backend starts an ordinary NO_SOURCE project with StartBuild and runs GitHub's own runner from billet's own JIT configuration, exactly as internal/provider/ec2 does inside an instance. See docs/reference/decisions/adr-007-codebuild-provider.md.
FOUR THINGS ARE UNLIKE EVERY OTHER BACKEND, and each of them shapes the code:
A BUILD CANNOT BE TAGGED. CodeBuild tags exist on projects and report groups, so the per-instance `sh.billet.owner` tag the ec2 backend filters List on has no equivalent. Ownership is a DEDICATED, tagged project plus per-build markers sent as environment-variable overrides and read back through BatchGetBuilds — and because List feeds a loop that stops builds, a project shared with an ordinary CodeBuild workload is a way for billet to stop somebody else's build.
THERE IS NO WAY TO LIST ONLY ACTIVE BUILDS, and history is kept for a year. ListBuildsForProject has no status filter, returns 100 ids a page newest-first, and errors if sortOrder is passed above 100 builds. So List and Find are a bounded page-walk, and what bounds them is CodeBuild's OWN enforced timeout: a build older than the declared build and queued ceilings cannot still be running.
StartBuild's idempotencyToken IS VALID FOR FIVE MINUTES. ec2's ClientToken was measured still refusing a changed relaunch of the same lease long after the fact, which is what makes that backend's retry-and-fallback loop safe. Here, past five minutes, an identical retry starts a SECOND build — two runners for one job. So the token covers a fast transport-level retry and nothing above it retries a launch: an ambiguous failure ASKS, which is what provider.Find is for.
AND A MANAGED BUILD HOST IS NOT A PER-JOB BOUNDARY. AWS documents a reserved-capacity instance as remaining alive between builds, shareable across projects, and as making cached data reachable by other projects in the account, by design — and macOS is reserved-only. So this backend refuses untrusted work outright rather than gating it on a network, because no security group fixes a machine that later runs somebody else's build.
Index ¶
- Constants
- Variables
- func BuildDescribeIAMActions() []string
- func BuildIAMActions() []string
- func BuildRoleKMSIAMActions() []string
- func BuildRoleLogIAMActions() []string
- func BuildRoleParameterIAMActions() []string
- func CheckReachable(ctx context.Context, cfg config.CodeBuildConfig, opts ...Option) error
- func FleetDescribeIAMActions() []string
- func JITKMSIAMActions() []string
- func JITParameterIAMActions() []string
- func ProjectDescribeIAMActions() []string
- func SetSSMEndpointForTest(p *Provider, endpoint string)
- func SetSleepForTest(p *Provider, sleep func(ctx context.Context, d time.Duration) error)
- func SetSweeperSSMEndpointForTest(s *RegistrationSweeper, endpoint string)
- func SweepIAMActions() []string
- func TimedOut(b build) bool
- type Ceilings
- type ClosureLookup
- type CredentialSource
- type FleetReport
- type LeaseClosure
- type Option
- type ProjectReport
- type Provider
- func (p *Provider) Accepts(trust provider.TrustClass) error
- func (p *Provider) Buildspec(spec Spec) (string, error)
- func (p *Provider) DescribeFleet(ctx context.Context) (FleetReport, bool, error)
- func (p *Provider) DescribeProject(ctx context.Context) (ProjectReport, error)
- func (p *Provider) Destroy(ctx context.Context, id string) (provider.Teardown, error)
- func (p *Provider) Find(ctx context.Context, name string) (*provider.Instance, bool, error)
- func (p *Provider) Kind() config.ProviderKind
- func (p *Provider) Launch(ctx context.Context, spec provider.Spec) (*provider.Instance, error)
- func (p *Provider) List(ctx context.Context) ([]*provider.Instance, error)
- func (p *Provider) Quotas(ctx context.Context) ([]provider.Quota, error)
- func (p *Provider) ReapStagedCredential(ctx context.Context, name string) error
- type RegistrationSweeper
- func (s RegistrationSweeper) Format(f fmt.State, _ rune)
- func (s RegistrationSweeper) GoString() string
- func (s RegistrationSweeper) LogValue() slog.Value
- func (s RegistrationSweeper) MarshalJSON() ([]byte, error)
- func (s RegistrationSweeper) String() string
- func (s *RegistrationSweeper) Sweep(ctx context.Context, closed ClosureLookup) (SweepReport, error)
- type Spec
- type SweepOption
- type SweepReport
Constants ¶
const AccountQueueCeiling = 30
AccountQueueCeiling is how many builds the WHOLE ACCOUNT may have queued.
MEASURED RATHER THAN LOOKED UP, on 2026-09-02 and recorded in docs/deploying/aws-codebuild.md: past thirty queued builds StartBuild is refused with `AccountLimitExceededException: Cannot have more than 30 builds in queue for the account`. Service Quotas does not list it, so it cannot be read — which is exactly why it is a constant here with the measurement attached rather than a number in a comment somewhere.
It is account-wide, so every CodeBuild project shares it.
const NameMarkerKey = nameEnvVar
NameMarkerKey is the environment-variable name carrying the lease name.
const OwnerMarkerKey = ownerEnvVar
OwnerMarkerKey is the environment-variable name carrying the deployment identity on every build billet starts.
EXPORTED BECAUSE A BUILD CANNOT BE TAGGED. Every other backend's ownership boundary is a tag, and awspolicy conditions its destructive statements on one — `aws:ResourceTag/sh.billet.owner`. There is no such condition available for a build, so the boundary here is the PROJECT (which can be tagged, and which awspolicy scopes the build actions to by ARN) plus this marker, which billet checks itself in List and Find. Exported so a policy generator and a diagnostic can name the same string billet writes.
Variables ¶
var ServiceInventoryWindow = time.Duration( (*config.CodeBuildConfig)(nil).InventoryWindowMinutes()) * time.Minute
ServiceInventoryWindow is how long after a lease closed its staged registration is still kept.
THE SERVICE MAXIMUM, NOT A NODE'S DECLARED CEILING. CodeBuild ends a build once its queued and build ceilings elapse, so a registration whose lease closed longer ago than the largest possible pair (plus the slack config already adds) cannot be read by anything again — whatever the node that staged it had declared, because every declared window is at most this one. Deriving it from the service rather than from a registration is what lets the sweep need nothing about a node but its path.
Functions ¶
func BuildDescribeIAMActions ¶
func BuildDescribeIAMActions() []string
BuildDescribeIAMActions are the read-only lookups reconciliation and teardown depend on.
ListBuildsForProject IS NOT OPTIONAL, and it is worth saying why a read looks load-bearing: it is the only route to an inventory, because CodeBuild offers no tag filter and no status filter. A node that cannot list cannot reconcile, and a node that cannot reconcile frees capacity for builds it can no longer see.
func BuildIAMActions ¶
func BuildIAMActions() []string
BuildIAMActions are what a node needs to start and stop its own builds.
SCOPED TO THE PROJECT ARN by the generator. StartBuild and StopBuild are the two that act, and neither can be conditioned on an ownership tag because a build carries none — so the project is the boundary, which is why the project must be billet's alone.
THE PROJECT ARN IS THE RIGHT RESOURCE FOR THE BUILD ACTIONS TOO, and this was MEASURED because it reads like a bug. A review argued that StopBuild and BatchGetBuilds act on BUILD resources, so a project-scoped grant would let the first launch succeed and then refuse every teardown and inventory. Tested against real AWS in us-west-2 on 2026-08-31 with a role holding exactly these five actions on one project ARN and nothing else: ListBuildsForProject, BatchGetProjects, BatchGetBuilds and StopBuild all SUCCEEDED, and the same role was correctly refused ListBuildsForProject on a project the policy does not name (AccessDeniedException). IAM's own simulate-custom-policy agrees, and additionally refuses a build-ARN-scoped statement against a build ARN — `build/...` is not a resource type these actions accept. Do not "fix" this by splitting the statement; it would grant nothing.
func BuildRoleKMSIAMActions ¶
func BuildRoleKMSIAMActions() []string
BuildRoleKMSIAMActions are what the build needs in order to decrypt its own registration under a customer-managed key.
func BuildRoleLogIAMActions ¶
func BuildRoleLogIAMActions() []string
BuildRoleLogIAMActions are what a build needs to write its own logs.
func BuildRoleParameterIAMActions ¶
func BuildRoleParameterIAMActions() []string
BuildRoleParameterIAMActions are what the BUILD's own service role needs.
A DIFFERENT PRINCIPAL FROM THE NODE, and the reason it has its own function rather than being folded into the set above: this role runs inside the compute that executes somebody's job. It reads one parameter path and writes logs. It may NOT start a build, may not delete a parameter, and may not reach any other deployment's path — a role that could start builds, running inside a build, is a way for a job to launch runners billet never escrowed capacity for.
GetParameters ALONE, MEASURED. `ssm:GetParameter` (singular) was in this list on the reasoning that either spelling might be the one CodeBuild calls, which is a guess about somebody else's API in a grant handed to job code. A real build in us-west-2 on 2026-08-31, whose service role held ONLY ssm:GetParameters, resolved its PARAMETER_STORE SecureString correctly — the buildspec reported the exact 48-byte length of the staged value. The singular is not called, so it is not granted.
func CheckReachable ¶
CheckReachable proves this identity can reach the API and see its own project.
ONE CALL, NO RETRY LADDER: check is interactive and rerunning it is cheaper than masking a flap.
func FleetDescribeIAMActions ¶
func FleetDescribeIAMActions() []string
FleetDescribeIAMActions are what `billet check` reads to report a reserved fleet's capacity and environment.
func JITKMSIAMActions ¶
func JITKMSIAMActions() []string
JITKMSIAMActions are what encrypting a SecureString under a customer-managed key needs.
SCOPED TO THE KEY, and conditioned by the generator on kms:ViaService for Parameter Store, so the grant cannot be used to decrypt anything else the key protects. The same shape the cache's EBS grant uses.
func JITParameterIAMActions ¶
func JITParameterIAMActions() []string
JITParameterIAMActions are what a node needs to stage and remove one build's single-use runner registration.
SCOPED TO THE CONFIGURED PATH by the generator, which is why node.codebuild.jit_parameter_path refuses a wildcard: the value lands in an IAM Resource ARN, and on a shared account the sibling paths a `*` admits are other deployments' runner registrations.
THERE IS NO GetParameter HERE. The node writes the registration and deletes it; it never reads one back. Granting a read would let a compromised node recover a credential it had already handed over, for no operational benefit — the BUILD's role is what reads it, and that is a different principal.
func ProjectDescribeIAMActions ¶
func ProjectDescribeIAMActions() []string
ProjectDescribeIAMActions are what `billet check` reads to report what a tier will actually get.
BatchGetProjects is also how billet answers the one question it must not get wrong about a project it did not create: whether a WORKFLOW_JOB_QUEUED webhook is attached, which would mean CodeBuild is acquiring jobs too.
func SetSSMEndpointForTest ¶
SetSSMEndpointForTest points a provider's Parameter Store client at a stand-in.
A NAMED SEAM RATHER THAN AN OPTION, and the distinction is the security property it protects. The Parameter Store endpoint is DERIVED from the region and deliberately NOT configurable: the single-use runner registration is written there, so an operator override would be a way to send a credential to a host of their choosing. `node.codebuild.endpoint` exists for a VPC interface endpoint or a non-commercial partition and covers the CodeBuild API alone.
A cross-package test still has to reach it — internal/e2e drives the real node runtime against a fake AWS — so this exists rather than widening the config. Its name says what it is for, it is documented as a test seam, and TestTheParameterStoreEndpointIsNeverTheConfiguredOne asserts the production derivation independently of it, so a mistake here cannot make that assertion vacuous.
It is NOT in a _test.go file because internal/e2e is a different package and Go does not export test helpers across one. That is the cost of the seam being honest about itself; the alternative was a config field with no legitimate use.
func SetSleepForTest ¶
SetSleepForTest replaces the pacing wait between teardown polls.
THE SAME KIND OF SEAM AS THE ONE ABOVE, for the same cross-package reason: internal/e2e proves what a node does with a teardown the backend never confirms, and confirmStopped paces its polls at teardownPollInterval — twenty seconds of wall clock per unconfirmed stop, proving nothing about waiting. A replacement must still honour the context, because that loop is bounded by cancellation as well as by a count and one that ignored it would spin.
func SetSweeperSSMEndpointForTest ¶
func SetSweeperSSMEndpointForTest(s *RegistrationSweeper, endpoint string)
SetSweeperSSMEndpointForTest is the same seam for the control plane's sweeper, which reaches Parameter Store through a client of its own.
func SweepIAMActions ¶
func SweepIAMActions() []string
SweepIAMActions are what the CONTROL PLANE needs to remove the registrations a dead node never reaped.
A THIRD PRINCIPAL, and the narrowest of the three. It LISTS the names under the path and DELETES the ones the ledger has proved dead — and that is all: there is no GetParameter, because the sweep never reads a registration; no PutParameter, because it stages nothing; and no KMS action, because a listing that asks for no decryption calls no key. The listing is GetParametersByPath rather than DescribeParameters because IAM can scope the former to exactly this path and cannot scope the latter at all, and a controller that could enumerate every parameter name in the account is wider than a sweep of one path needs.
THE MISSING KMS ACTION IS NOT WHAT KEEPS A REGISTRATION OUT OF THE CONTROLLER, and that was MEASURED rather than assumed (2026-09-02): a real role holding exactly this grant and nothing else, asked for the listing WITH decryption, received plaintext — the account's aws/ssm key authorises any principal that reaches it through Parameter Store, so an identity policy's silence about KMS bounds nothing there. What keeps the value out is the sweep's request stating WithDecryption=false and its response type having no Value field, both pinned by tests; a customer-managed key is what makes this grant decisive again, because decryption then needs a kms:Decrypt it does not carry — measured under a fresh key the same day: the decrypting listing was refused kms:Decrypt, the delete still succeeded, and a page mixing one customer-key parameter with one default-key parameter failed whole rather than handing back the default one.
GetParametersByPath IS AUTHORISED AGAINST THE PATH ITSELF, so the generator renders the grant on both the path and the path's children — the first for the listing, the second for each delete. MEASURED on 2026-09-02, twice: with iam:SimulateCustomPolicy, and then under that real role, whose listing succeeded with both resources and was refused `ssm:GetParametersByPath on resource: …:parameter<path>` once the grant named only `<path>/*` — the service naming the PATH as what it authorised against. A grant naming only the children looks scoped and lists nothing.
func TimedOut ¶
func TimedOut(b build) bool
TimedOut reports whether a terminal build was ended by CodeBuild's own timeout rather than by the job failing.
REPORTED DISTINCTLY because the two send an operator to different places: a FAILED build is somebody's test, and a build the service ended at its ceiling is a limit this backend cannot lift. Filing the second as the first is how a fleet-level limitation is mistaken for a flaky suite.
THE STATUS IS NOT WHERE THE TIMEOUT IS, and the first version read only that. MEASURED against real CodeBuild in us-west-2 on 2026-08-31 — a build with timeoutInMinutes 5 whose BUILD phase slept 400s came back:
buildStatus FAILED phases[BUILD].phaseStatus TIMED_OUT phases[BUILD].contexts[0] BUILD_TIMED_OUT: Build has timed out.
So `buildStatus == "TIMED_OUT"` was a predicate that could never fire for the case it exists for, and every build the ceiling ended would have been reported as a failing test. TIMED_OUT is still a DOCUMENTED build status, so it is kept rather than replaced: the question is asked of the status AND of every phase, because a rule about somebody else's API that only one of its two spellings satisfies is the same defect one spelling over.
Types ¶
type Ceilings ¶
type Ceilings struct {
// BuildMinutes and QueuedMinutes are what this node declared, which is what
// billet sends. They are the OPERATOR's numbers.
BuildMinutes int
QueuedMinutes int
// ServiceBuildMinutes and ServiceQueuedMinutes are the service's own maxima,
// so a report can say whether the declared numbers are the ceiling or a choice
// inside it.
ServiceBuildMinutes int
ServiceQueuedMinutes int
// InventoryWindowMinutes is how far back List has to walk before an absence is
// conclusive — derived from the declared ceilings, and worth reporting because
// it is the cost a longer ceiling buys.
InventoryWindowMinutes int
// Reserved says whether this node draws on a reserved-capacity fleet, which
// decides both the isolation story and the cost shape.
Reserved bool
// MacOS says whether this node runs Apple silicon, which is reserved-only.
MacOS bool
}
Ceilings is what a CodeBuild tier inherits from the service and cannot escape.
REPORTED AS A CAPABILITY rather than buried in a config comment, because the backend's acceptance turns on it: an operator has to see the 36-hour cap and the 8-hour queued cap before work is admitted. The alternative is meeting the first one as a build that died at hour 36.
THE CONCURRENCY QUOTA IS NOT HERE, and this comment used to claim it was. It is an ACCOUNT limit rather than a service one — read from Service Quotas by Quotas() in quota.go, which needs credentials and a network where everything below needs neither. Keeping the two apart is what lets `billet check` report these on a laptop with no AWS access at all, which is the one moment the sentence is most useful.
func CeilingsFor ¶
func CeilingsFor(cfg config.CodeBuildConfig) Ceilings
CeilingsFor reports what a configured node's jobs inherit, without calling AWS.
type ClosureLookup ¶
type ClosureLookup func(ctx context.Context, leaseID string) (LeaseClosure, error)
ClosureLookup answers LeaseClosure for one lease id, from the ledger.
AN ERROR STOPS THE PASS. A ledger that could not answer is evidence about nothing, so the sweep deletes nothing further and reports the failure rather than reading silence as permission — the could-not-tell/no rule this repository applies to every deletion.
type CredentialSource ¶
CredentialSource resolves AWS credentials.
AN ALIAS FOR awscreds.Source SINCE THE CHAIN MOVED, and it used to be a declaration. The chain lived in internal/provider/ec2, so importing it would have made one compute backend a library for a sibling — this package therefore declared its own interface over the same method and cmd/billet adapted ec2's chain into it. The chain is a shared package now, and the second interface has nothing left to be: what it named is what awscreds.Source names.
KEPT AS A NAME rather than deleted, because a caller reading this package's constructor should see what kind of thing it wants without following an import.
THERE IS NO DEFAULT. New refuses a nil source rather than reaching for one, so a caller cannot end up signing with credentials it did not choose.
type FleetReport ¶
type FleetReport struct {
Name string
ARN string
// EnvironmentType must match the node's, or every launch is refused by AWS.
EnvironmentType string
ComputeType string
// BaseCapacity is how many builds can run at once, which is the number a
// macOS tier's macos_vm_limit should be set to.
BaseCapacity int
// MaxCapacity is the scaling ceiling when one is configured, or zero.
MaxCapacity int
Status string
StatusNote string
// StatusContext is AWS's reason beside the status code, when it has one. An
// ACTIVE fleet AWS cannot find an instance for reports INSUFFICIENT_CAPACITY
// here and looks healthy by Status alone.
StatusContext string
}
FleetReport is what a reserved fleet turned out to be.
func (FleetReport) ConcurrencyProblems ¶
func (r FleetReport) ConcurrencyProblems(declared int) ([]string, []string)
ConcurrencyProblems judges the macOS concurrency a deployment declared for this node against what the fleet can actually run at once.
declared is the node's effective macOS cap — nodes[].macos_vm_limit, which every macOS tier pinned to the node is validated against — and a zero means the caller established none, in which case there is nothing to compare and nothing is said. THE CALLER DECIDES WHETHER THE COMPARISON APPLIES: a Linux fleet runs no macOS build whatever the policy says, so judging one against a limit that is legal to declare and used by nothing refuses a working deployment (cmd/billet gates on the environment type).
A REFUSAL, NOT A WARNING, and the measurement is why. With a base capacity of 1 and a declared limit of 2, billet did exactly what the config asked: it escrowed two jobs and started two builds, and the second sat QUEUED behind the busy Mac. GitHub withdraws an assignment that is not acquired within about five minutes, so that queued build was stopped, the job requeued, a fresh build queued, and the cycle repeated until the first job finished — every build correct, every one a StartBuild against the account's 30-wide queue, and the job's wall clock the first job's duration plus a requeue. A declared limit above the fleet is therefore capacity the fleet does not have, advertised to GitHub as if it did, and MAC_ARM offers no overflow to on-demand (`Fleet on-demand overflow behavior is not supported for MAC_ARM`, measured), so nothing on the AWS side can absorb it. The fix is one number in the config, which is what the message names.
The bound is the fleet's BASE capacity. A scaling configuration's maximum is what AWS may grow to, not what it holds, and a build queued while the fleet scales is the same queue as above — so a limit the maximum covers is a WARNING that says so, for an operator who has measured their fleet scaling fast enough.
AND A CAPACITY BILLET COULD NOT READ IS SAID, NOT PASSED. CreateFleet refuses a base capacity below one, so a zero here is a description that omitted the field rather than a fleet with no machines; "could not tell" is still not "fine", and the warning says which number went unchecked.
func (FleetReport) Problems ¶
func (r FleetReport) Problems(cfg config.CodeBuildConfig) ([]string, []string)
Problems reports what a fleet's shape means for this node.
type LeaseClosure ¶
type LeaseClosure struct {
Known bool
Terminal bool
// FinishedAt is when the ledger closed the lease. Zero means it cannot say,
// which is never old enough.
FinishedAt time.Time
}
LeaseClosure is what the ledger knows about whether one lease is over.
THREE ANSWERS, NOT TWO, and the caller supplies them. Known=false is a lease the ledger has never heard of; Terminal=false is one still open; Terminal=true is one that released its capacity, closed at FinishedAt. Only the third, aged past ServiceInventoryWindow, authorises a delete. The first is reported and kept: it is what a ledger restored from an older backup looks like, and a build may be running under it.
type Option ¶
type Option func(*Provider)
Option configures a Provider.
func WithCredentials ¶
func WithCredentials(src CredentialSource) Option
WithCredentials sets where AWS credentials come from. REQUIRED: there is no default, so a caller cannot end up signing with credentials it did not choose.
func WithHTTPClient ¶
WithHTTPClient sets the client used for API calls, for a test or for a deployment that needs a proxy.
func WithLogger ¶
WithLogger sets the logger. The default is slog.Default().
type ProjectReport ¶
type ProjectReport struct {
Name string
ARN string
// EnvironmentType and ComputeType are the project's own settings. billet
// OVERRIDES both on every launch, so a mismatch is reported rather than
// refused — but it is worth seeing, because an operator reading the console
// will see the project's numbers rather than billet's.
EnvironmentType string
ComputeType string
// SourceType should be NO_SOURCE. billet pins it on every launch, so this is
// reported rather than refused; a project with a source configured is a sign
// somebody is using it for something else.
SourceType string
// OwnerTag is the deployment identity tagged on the project, or empty. THE
// PROJECT IS HALF THE OWNERSHIP BOUNDARY, because a build cannot be tagged, so
// a project carrying another deployment's tag is the thing to catch here.
OwnerTag string
// RunnerWebhook is set when the project carries a WORKFLOW_JOB_QUEUED webhook.
//
// THE ONE FINDING THAT IS A REFUSAL RATHER THAN A REPORT. It means CodeBuild is
// acquiring jobs too, and two schedulers on one job produce duplicate runners —
// a failure that looks like GitHub misbehaving rather than like a configuration
// mistake.
RunnerWebhook bool
}
ProjectReport is what a project turned out to be.
func (ProjectReport) Problems ¶
func (r ProjectReport) Problems(owner string) ([]string, []string)
Problems reports what a live look found that an operator has to act on.
SPLIT FROM THE DESCRIBES so the reports stay plain data and this stays the one place that judges them — which is also what lets `billet check` print the facts even when one of them is a problem.
EXACTLY ONE OF THESE IS FATAL and the rest are warnings, and the distinction is whether billet would do the wrong thing or merely a surprising one. A competing runner webhook makes two schedulers acquire one job, which produces duplicate runners billet never escrowed capacity for; everything else is a mismatch an operator should see and that billet overrides on every launch anyway.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider launches CodeBuild builds, one per job.
func New ¶
New builds a CodeBuild provider. owner names this billet deployment and is written into every build it starts.
func (*Provider) Accepts ¶
func (p *Provider) Accepts(trust provider.TrustClass) error
Accepts reports whether this backend may run work of that trust class.
UNTRUSTED IS ADMITTED ONLY ON AN ON-DEMAND CONTAINER TIER WITH A CONFIGURED ISOLATED NETWORK, and that is a narrower door than the ec2 backend's for a measured reason: a reserved-capacity fleet instance stays alive between builds and shares cached data with other projects in the account, by design, so no network fixes a machine a later build inherits — and macOS is reserved-only. An on-demand Linux build, by contrast, is a freshly-booted machine destroyed with the build (31 builds, 31 distinct host boot-ids, nothing surviving between them, measured 2026-09-02), which makes it a per-job boundary the way an ec2 instance is — after which the network is the only remaining question, exactly as it is for ec2.
THE DECISION AND THE NETWORK IT REQUIRES ARE ONE FUNCTION, untrustedNetwork, so the launch cannot admit a class this refuses or run it off the network this names.
UNKNOWN IS REFUSED for the separate reason it always is: untrusted is a classification billet made, unknown means it could not classify the job at all, so there is no basis for choosing anything.
func (*Provider) Buildspec ¶
Buildspec is the document billet sends as buildspecOverride.
BILLET GENERATES IT, for the reason ADR-002 gives about not shipping a Packer template: the contract between billet and its runner is a few lines of shell, and a project-owned buildspec is a second place for them to disagree. Terraform creates the project with a minimal placeholder so CreateProject validates.
IT NEVER MENTIONS THE REGISTRATION. CodeBuild resolves the PARAMETER_STORE variable into the environment before any phase runs, so the runner reads it from there and no command here has it to leak. Nothing below refers to jitEnvVar.
AND IT IS EXECUTED IN A TEST rather than pattern-matched. That is the ec2 boot-script lesson: its first version carried the registration in a quoted heredoc inside `$( )`, which reads safer than a plain assignment and is not — a single quote in the value confused the shell scanning for the closing paren and /bin/sh died with "unexpected EOF" on a later line. A boot script that fails to parse is compute that starts, registers nothing, and reports success.
func (*Provider) DescribeFleet ¶
DescribeFleet reports what the configured reserved fleet is, or that none is configured.
func (*Provider) DescribeProject ¶
func (p *Provider) DescribeProject(ctx context.Context) (ProjectReport, error)
DescribeProject reports what the configured project is.
A PROJECT THAT RESOLVES TO NOTHING IS AN ERROR rather than an empty result: the check asked about a specific project, and "there is no such project" is the answer it needs.
func (*Provider) Destroy ¶
Destroy stops a build and waits for it to actually stop.
IDEMPOTENT: a build that is already gone or already finished is success, because teardown runs on paths that have already failed once and an error there turns a recoverable state into a stuck one.
IT POLLS TO A TERMINAL STATE BEFORE CLAIMING ONE. StopBuild is a REQUEST — the same lesson `tart stop` taught one backend over, and the same lesson TerminateInstances taught the other: reading the state once immediately afterwards catches a build mid-transition and reports it as still running, while returning TeardownStopped on the strength of the request being accepted frees capacity for a job that is still executing somebody's deploy. Only a status CodeBuild positively reports as terminal earns TeardownStopped; everything else — including running out of polls — returns TeardownRequested, which keeps the capacity charged and hands the question to custody.
func (*Provider) Find ¶
Find reports the build with that name, and whether there was one.
A WALK RATHER THAN A LOOKUP, because CodeBuild offers no way to ask. There is no tag to filter on and no status filter on the listing, so the only route is recent history — bounded by the fact that CodeBuild itself ends a build once the declared ceilings elapse.
IT INCLUDES TERMINAL BUILDS, unlike List, and that difference is the same one the ec2 backend makes between findStates and liveStates: a targeted lookup wants the terminal record as CAUSAL PROOF for custody, while fleet inventory must not carry a year of history for reconciliation to re-tear-down.
func (*Provider) Kind ¶
func (p *Provider) Kind() config.ProviderKind
Kind reports the backend this is.
func (*Provider) List ¶
List reports every build this backend is running for billet.
THE INPUT TO RECONCILIATION, which frees the capacity of every lease ABSENT from it. So the dangerous failure is not an error — an error stops reconciliation — but an answer that is short, empty and successful.
IT FAILS WHOLE RATHER THAN SHORT. A build carrying this deployment's owner marker whose lease marker names no lease is compute billet cannot account for, and omitting it would hand back the capacity of something still running. The message names the build and both remedies, because failing closed stops this node's sweep until somebody intervenes — which is the right direction only if an operator is told what to do.
TERMINAL BUILDS ARE EXCLUDED, the liveStates half of the ec2 split: CodeBuild retains a year of history and reconciliation would otherwise be handed a year of corpses to stop on every pass.
func (*Provider) Quotas ¶
Quotas reports the account ceilings this node's configuration runs against.
THIS IS THE SENTENCE docs/deploying/aws-codebuild.md ALREADY PROMISED AND NOTHING SUPPLIED. checks.go's own doc comment said an operator "has to see the 36-hour cap, the 8-hour queued cap, and the concurrency quota before work is admitted", and Ceilings carried the first two and no quota at all — a claim in a comment the code did not support. That document's own measurement makes the case sharper than the comment did: the default is ONE per compute type, the account-wide queue is capped at thirty, and past either the overflow becomes FAILED jobs rather than slow ones, because GitHub requeues a job at most three times.
IT LISTS RATHER THAN NAMING CODES. CodeBuild has one concurrency limit per environment and compute type and their identifiers are not derivable from anything billet knows, so billet asks AWS what they are and matches the names AWS returns. Shipping a table of quota codes would be billet inventing identifiers for somebody else's API — and a wrong one reads as "no limit", which is the direction that costs somebody's build.
A PARTIAL ANSWER AND AN ERROR TOGETHER, which is the QuotaReporter contract: one shape billet could not find a limit for must not discard the ones it did.
func (*Provider) ReapStagedCredential ¶
ReapStagedCredential removes a lease's staged runner registration.
provider.StagedCredentialReaper. A CodeBuild build cannot be handed a secret — `StartBuild` has no field for one — so the registration lives in Parameter Store and OUTLIVES the build. Destroying the compute does not remove it, which is what makes this a contract rather than part of teardown.
THE CALLER'S PROOF IS THE AUTHORISATION, and the first version tried to derive its own. It asked `findBuild` and deleted when the build looked consumed OR when no build was found at all — and that second branch is the unsafe one: a `StartBuild` that committed and lost its response leaves a build the inventory has not listed yet, so "I cannot find it" is indistinguishable from "it has not appeared", and deleting there strands exactly the build the ambiguous-launch path deliberately keeps the parameter for. Billet calls this only where the compute is already PROVED gone — custody settlement, and the failed-launch cleanup that confirms on an explicit terminal record — so there is nothing left to ask. See provider.StagedCredentialReaper: the precondition is the proof rather than any one call site.
AND IT WAS NEVER CALLED, which is the defect underneath the unsafe one: its own doc comment said the node's tending sweep invoked it and nothing in production did. So every lease that settled without a Destroy — which is the ordinary case here, since the runner exits and the build ends on its own — left its parameter behind until somebody hit the account's Parameter Store quota.
AN ABSENT PARAMETER IS SUCCESS, because the teardown path deletes it when it confirms a build terminal and this is then a second call about something already gone.
type RegistrationSweeper ¶
type RegistrationSweeper struct {
// contains filtered or unexported fields
}
RegistrationSweeper lists one Parameter Store path and removes the registrations the ledger has proved dead.
func NewRegistrationSweeper ¶
func NewRegistrationSweeper( region, path string, creds CredentialSource, opts ...SweepOption, ) (*RegistrationSweeper, error)
NewRegistrationSweeper builds a sweeper for one path in one region.
THE PATH IS RE-VALIDATED HERE because it arrived over the wire rather than through config.Load — the alloc.New rule — and because it is a prefix under which this process will DELETE. Parameter Store's endpoint is derived from the region and never configurable, for the reason the provider's is: it is where registrations live.
func (RegistrationSweeper) Format ¶
func (s RegistrationSweeper) Format(f fmt.State, _ rune)
Format catches every verb.
func (RegistrationSweeper) GoString ¶
func (s RegistrationSweeper) GoString() string
GoString covers %#v.
func (RegistrationSweeper) LogValue ¶
func (s RegistrationSweeper) LogValue() slog.Value
LogValue is what slog consults; its JSON handler ignores fmt entirely.
func (RegistrationSweeper) MarshalJSON ¶
func (s RegistrationSweeper) MarshalJSON() ([]byte, error)
MarshalJSON keeps a sweeper out of anything that serializes it structurally.
func (RegistrationSweeper) String ¶
func (s RegistrationSweeper) String() string
REDACTED, because it holds a credential source through an unexported field of an unexported struct, and fmt cannot invoke methods through either. Same five methods as the client, same value receiver.
func (*RegistrationSweeper) Sweep ¶
func (s *RegistrationSweeper) Sweep(ctx context.Context, closed ClosureLookup) (SweepReport, error)
Sweep lists the path once and removes every registration whose lease the ledger has proved closed for longer than the service window.
THE REPORT IS RETURNED BESIDE AN ERROR, deliberately: a pass that stopped part-way still removed and kept what it counted, and a caller recording the pass should record that alongside why it stopped.
type Spec ¶
Spec is the subset of provider.Spec this package's buildspec builder needs.
DECLARED SEPARATELY SO THE BUILDSPEC CAN BE TESTED WITHOUT A PROVIDER, and deliberately WITHOUT the JIT config: the buildspec must not be able to carry the registration even by accident, and a type that does not hold it cannot.
type SweepOption ¶
type SweepOption func(*RegistrationSweeper)
SweepOption configures a RegistrationSweeper.
func SweepWithClock ¶
func SweepWithClock(now func() time.Time) SweepOption
SweepWithClock replaces the clock the window is measured against.
func SweepWithHTTPClient ¶
func SweepWithHTTPClient(c *http.Client) SweepOption
SweepWithHTTPClient sets the client used for API calls, for a test or a proxy.
func SweepWithLogger ¶
func SweepWithLogger(log *slog.Logger) SweepOption
SweepWithLogger sets the logger. The default is slog.Default().
type SweepReport ¶
type SweepReport struct {
Region string
Path string
// Removed is what the pass deleted.
Removed int
// Kept is waiting on a lease that is open or closed too recently.
Kept int
// Unaccounted names lease ids the ledger has never heard of. Kept, and worth
// a person's look.
Unaccounted int
// Foreign is entries under the path that are not billet's at all. Kept.
Foreign int
}
SweepReport is what one pass over one path found and did.