Repository navigation
Shapes on every capacity, Fargate capacities, vCPU budgets and swap repair (#18) - #32
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each architecture builds and smoke-tests on its own runner (ubuntu-latest, ubuntu-24.04-arm). On main each pushes by digest, and a publish job tags the manifest list of both as :latest and :<date>-<sha>, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The template is past the 51,200 bytes CloudFormation takes inline, so both upload it to $(STACK)-deploy-<account id>, made outside the stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-image Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All four cover the default budgets (24 / 8 / 24 / 8), so no increase was requested. Fargate Spot and On-Demand are already at 140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and architecture Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runner with its shape Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The webhook's swap repair re-queues a runner's original job from its instance's tags, and the launcher mints with the message's labels, so the instance carries them as ci-runner:labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a stack runner starts a job other than the one it was launched for, the webhook re-queues that job from the instance's tags and re-tags the instance with the job it runs, so no job is stranded and a Spot reclaim is reported on the right commit (ADR-0003, #21). Its role gains ec2:DescribeInstances and ec2:CreateTags on the stack's instances, for the job tags only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sodre
added a commit
that referenced
this pull request
Nov 14, 2026
## Summary Landed early from the integration branch (#32), because only `main` publishes. After this merges, `ghcr.io/zeroae/ci-runner:latest` is a multi-arch manifest list. ```diff image.yml - build (ubuntu-latest, linux/amd64) → smoke → push :latest, :<date>-<sha> + build-amd64 (ubuntu-latest) → smoke EXPECT_ARCH=x86_64 → push by digest + build-arm64 (ubuntu-24.04-arm) → smoke EXPECT_ARCH=aarch64 → push by digest + publish (main only): imagetools create → :latest, :<date>-<sha> ``` `image/smoke.sh` now checks that `uname -m` and every binary the image adds (docker, buildx, compose, gh, node, aws) are built for the expected architecture. ## Evidence - **Before:** `:latest` is a single linux/amd64 image. - **After:** a branch run builds both architectures natively and passes the smoke test on each: https://github.com/zeroae/ci-runner/actions/runs/43539165229. Locally, `EXPECT_ARCH=x86_64` on an arm64 machine fails every architecture check, and `aarch64` passes. - **Not yet run:** the publish job, which only runs on `main`. I'll watch the first run after merge and check that `:latest` lists amd64 and arm64. ## Merge Danger **Door:** two-way. Reverting brings back the single-arch build; the next weekly or `image/**` build republishes `:latest`. **Blast Radius:** runners. Every EC2 CI runner pulls `:latest` at boot. An amd64 host pulls the amd64 entry of the manifest list, so it gets the same image as before. If the publish job fails, `:latest` simply isn't updated. Part of #18, closes #23 on the integration side. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpotVcpuCap (24) and OnDemandVcpuCap (8) replace SpotCap and OnDemandCap. The launcher sums the vCPUs of a capacity's live instances from their shape tags, across both architectures; an instance from before the tags counts as 4 on the capacity its InstanceLifecycle says. A job that would go over its budget is deferred, logged as "20+8 of 24 spot vCPUs". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The override examples and the traps table use SpotVcpuCap and OnDemandVcpuCap and name the EC2 quota each draws on; the deferral line is in vCPUs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… template Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…both The conventions test now reads env var names with digits (ARM64_...). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nch template Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ution role Live, every Fargate launch failed: RunTask passes the task definition's execution role, and the PowerUserAccess boundary allows no iam:PassRole at all. The account's ZAEPowerUserBoundary is PowerUserAccess plus iam:PassRole on PowerUserPB* roles, and the launcher's own policy narrows that to the execution role, passed to ECS tasks only. The conventions test now requires that boundary on a role that passes one, and PowerUserAccess on every other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are offered at .large to .4xlarge in all three of the stack's AZs (use1-az1/az2/az4), and their .xlarge Spot prices are at or below m7i's. Six families x three subnets = 18 fleet overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into ticket/29-fargate-capacities
…er job aws-emulator drops a task's tags once it is told to stop, so the first SpotInterruption change (the warning, while the task runs) may be the only one the notifier can read. The notifier now skips a reclaim already reported on the commit, for instances too. Emulator tests cover the task's swap, sweep and reclaim against real ECS and EventBridge shapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into ticket/30-tasks-lifecycle
…roof pending Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live, a Fargate task's idle exit logged "no job within 120s, stopping" and the task kept running: run-helper.sh and Runner.Listener sit in their own process group inside the runner's session, so the TERM sent to the session leader's group missed them and the wrapper waited forever. The stop now signals the whole session (pkill -s), then KILLs it after IDLE_GRACE_SECONDS (default 30, as docker stop gives an EC2 runner). With a real JIT runner the session-wide TERM alone stops it at once. The smoke test adds a fake runner like the real one: it ignores TERM and has a child in another process group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher's role needs ZAEPowerUserBoundary for iam:PassRole; the idle exit stops the runner's whole session, then KILLs it after a grace; the Docker trap quotes the error a Fargate job really gets; two new traps for the PassRole failure and a stale idle exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into ticket/29-fargate-capacities
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured live on 2026-11-14: after StopTask, describe-tasks --include TAGS returned the task's tags through DEPROVISIONING and STOPPED and minutes after, while ListTagsForResource refused the task as stopped. The docs said ECS drops them, which only aws-emulator does. No code relied on it: the sweeper, launcher and webhook list desired-RUNNING tasks, and the notifier's check-run lookup keeps a reclaim to one report per job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… STOPPED The budget sums the tasks not yet STOPPED, but listed only desired RUNNING, so a task told to stop but still deprovisioning dropped out early. List the desired-STOPPED tasks too and count those whose last status isn't STOPPED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 4 vCPUs / x64 default for old messages and untagged machines was written in four places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ader Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
webhook: shape() -> shapeOf(), returning the capacity label as capacity (the queued message keeps its label field); sh -> shape, caps -> capacities, and the job a swapped runner ran is ran, not now. notifier: real -> ghJob, done -> existing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an object Each file keeps its own copy (no shared module, per docs/stack.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tags Measured live (6dbaa56); only aws-emulator drops them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re *VcpuCap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Docker promise Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Launch Spot or On-Demand with the launcher's fleet options, and label and tag the runner with that capacity. A Fargate capacity is refused with a pointer to the runbook's "Run a task by hand". The LABEL override is gone: CAPACITY is the label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sodre
marked this pull request as ready for review
November 14, 2026 04:45
With CI_RUNNER_VCPUS set, ci-runner-idle-exit writes /home/runner/.cargo/config.toml with [build] jobs = <n>, leaving an existing file alone. A CARGO_BUILD_JOBS in the environment changes Swatinem/rust-cache's key (it hashes every CARGO*, CC*, CFLAGS*, CXX*, CMAKE* and RUST* variable), so a Fargate job never hit the cache an EC2 job saved. Cargo reads .cargo/config.toml from every ancestor of its working directory, so the file reaches every checkout under _work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swatinem/rust-cache hashes every CARGO*, CC*, CFLAGS*, CXX*, CMAKE* and RUST* environment variable into its key, so a task's CARGO_BUILD_JOBS and CMAKE_BUILD_PARALLEL_LEVEL gave Fargate jobs a key of their own: they never hit the cache an EC2 job saved. A task now gets CI_RUNNER_VCPUS, which the image's entrypoint turns into ~/.cargo/config.toml's [build] jobs, and keeps MAKEFLAGS=-j<n>. stack.md, the runbook (with a traps row) and the README say why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live at 48 since the zae-zcm sample: at 24, a 9-job matrix at ci-cpu-8 spent most of its queued time waiting on the budget. The Fargate Spot quota is 140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Spec #18. Every job names its full shape, and every capacity serves every shape. Two new Fargate capacities,
ci-fargate-spotandci-fargate, have no Docker daemon (ADR-0001). Caps become vCPU budgets, and swaps are repaired instead of prevented (ADR-0003). Terms are inGLOSSARY.md.Infrastructure:
main(the repo's immutable OIDC subject), reusing the account's existing OIDC provider (ADR-0002);ZAEPowerUserBoundary, so it can pass the task execution role.The image work landed on
mainalready (#33–#36): the multi-arch build, the SOCI/ECR push and the idle-exit wrapper.Evidence
Each was proven live on the deployed integration branch. The evidence is on each ticket.
.xlargeci-cpu-8→m6a.2xlarge,nproc8m8g.xlargeand Fargate both printaarch64"deferred": "4+4 of 4 on-demand vCPUs", then launchedSpotInterruptionproducedci-runner / j (4)with the real job name; a repeat logsalready reportedmake test: 144 pass, including the aws-emulator ECS tests. The template is 74,337 of 1,048,576 bytes.Release note: breaking label change
From this deploy on, a job must carry a size label and an architecture label.
runs-on: [self-hosted, ci-spot]is now outside the contract, and the job waits for a runner that never comes. Use, for example,[self-hosted, ci-spot, ci-cpu-4, x64], which is the old shape. Every repo in theci-runnerrunner group was updated before the change went live (ci-runner-canary#2, zae-zcm#37, foresight#1194).SpotCapandOnDemandCapare replaced bySpotVcpuCap(24) andOnDemandVcpuCap(8), plusFargateSpotVcpuCap(48) andFargateVcpuCap(8). An override on the old names is dropped.Merge Danger
Door: two-way for the code, one-way for the label contract in practice. The integration branch is already what the live stack runs, so this merge changes nothing live. Rolling back means redeploying
main's template, which brings back the defaults and drops the Fargate capacities and the new parameters.Blast Radius: every consumer's CI.
A job missing a size or architecture label now hangs; the runbook's traps table covers it. Budgets are soft (the owner's choice): a burst can exceed them, and this was seen live at 36–44 of 24 Spot vCPUs. The launcher's role depends on the account's
ZAEPowerUserBoundary, which lives outside this stack.Not proven live: a real Spot reclaim on Fargate, and a sweeper stop of a task older than 2 hours. #31, the human canary, stays open for those.
Closes #18
Closes #19, closes #20, closes #21, closes #22, closes #23, closes #24, closes #25, closes #26, closes #27, closes #28, closes #29, closes #30
🤖 Generated with Claude Code