GitHub’s cache, kept in your own AWS account: each region your machines run in gets a bucket of the control plane’s, and a free S3 endpoint in its network, so a cache never leaves your account and never goes over the internet. Nothing in your workflows changes: actions/cache, the setup actions’ caches, Docker’s type=gha and the rest reach it as they reach GitHub’s. On a 16-CPU machine a 4 GB cache saved in 10 s and was back in 11 s (GitHub’s own: 12 s and 21 s).
Each machine runs superci-cache, which holds no keys: for every lookup and save it asks the control plane with the job’s own token, and the control plane checks it against GitHub’s keys and signs links to the bucket that each do one thing. A job reads and writes what GitHub would let it: a pull request reads its own branch’s caches, then its base’s and the default branch’s, and writes only its own.
On unless turned off, and what it keeps is yours to set (Settings → Cache, or superci caches update --enabled=false --size-gb=25 --days=7): each repository 25 GB, the least recently used going first, and an entry unused for 7 days goes. An entry saved 30 days ago is saved anew whatever its use, and the bucket removes anything older than 32 days by itself.
superci caches retrieve says what each repository keeps, superci caches list --repo=OWNER/NAME its entries (key, branch, size, last use), and superci caches delete --repo=OWNER/NAME [KEY] --confirm takes them out; Settings → Cache clears a repository with a button.
Caches saved at GitHub before are used until they age: a lookup not found in your bucket is asked of GitHub. When the control plane cannot be reached, jobs use GitHub’s cache as before. Artifacts of GitHub’s jobs stay at GitHub.
Turbo’s, Nx’s and Bazel’s remote caches go to the same bucket, with nothing to set: each machine tells them where (Turbo and Nx in the runner’s environment, Bazel in its system-wide settings), and a workflow’s own settings still come first (a Vercel token goes on to Vercel). They carry no GitHub token, so what they may read and write is what GitHub said of the job: its branch’s to write, then the default branch’s to read; a branch never writes where the default branch reads.
Windows machines on AWS use it too, and GitLab jobs on AWS keep GitLab’s own cache there (GitLab’s runner is pointed at it, as S3), by project and by the branch GitLab said the job runs for.
The control plane’s role may read, write and remove what is in its own buckets (CacheBucket); the bucket and the endpoint are made by the dashboard with your sign-in, beside each region’s network. Machines on Cloudflare and Modal keep using GitHub’s cache.
Runs from a computer: superci runs create --workflow=.github/workflows/ci.yml runs a workflow’s jobs as GitHub would run them, each on a machine as any job of your control plane’s, with the working tree as it is on your computer and nothing pushed. What git tracks and the new files it does not ignore go along (an ignored .env stays behind), through your control plane: no new resource or permission in your cloud. It is kept there only until the run’s jobs have fetched it. GitHub’s own runner runs the steps, as in CI: actions, service containers, expressions, step outputs.
What GitHub would give such a job and cannot here is given on the command line: --secret=NAME=VALUE (hidden in the job’s lines as GitHub hides secrets, and not kept once the job has ended) and --var=NAME=VALUE; --event=pull_request when the workflow does something else for one. There is no GITHUB_TOKEN and no OIDC.
Their jobs use the cache as CI’s do: they read the caches of their branch and the default branch (what GitHub’s jobs saved), and write a scope of their own, so a run from a computer never changes what CI finds. Their artifacts are kept in the same bucket for as long as the run (a day): upload-artifact in one job and download-artifact in the next work as on GitHub.
As GitHub runs them: each combination of a matrix is a job of its own, on the machine its runs-on says; a job starts once the jobs it needs have ended, runs if its if holds (by default, if they all succeeded), and knows their results and outputs. --job=NAME runs that job and those it needs.
The command waits for the run and prints its jobs’ lines; it fails when the run does. Its jobs are listed with the others (superci jobs list, Runs), with their machines and their cost.
A key for coding agents: superci keys create agent --runs makes a key that starts such runs and changes nothing else, within limits (--max-cpu=16 CPUs a job, --daily-usd=10 of machines a day, three runs at a time). With it and your control plane’s address, superci runs create works with no sign-in.
Each run (a workflow run on GitHub, a pipeline on GitLab, a run from a computer) has a page too: how it stands, how long it took, what its machines cost, the job it waited for last, and its jobs, each opening its own page; GitHub’s or GitLab’s page of it is a button there, with its logo. A job’s page opens its run’s.
A GitLab job that ran and failed is said as failed by itself, as a GitHub job is, not as a job SuperCI could not start. Its time is GitLab’s own, from when it began to when it ended (a job a sweep first saw could show 0 s).
Each job has a page in the dashboard: how it stands, its machine and cost, how its start went step by step, and its log, looked at again every few seconds while it runs. Its page at GitHub or GitLab is a button on it; a run from a computer, which has no such page, is followed there.
A GitLab job whose event GitLab never sent still gets its machine: every three minutes the control plane asks GitLab for the waiting jobs of the projects that send theirs here, as it already asks GitHub, and it asks GitLab how a job went when its events do not come.
A job started by the minute’s sweep (one GitHub or GitLab never told of, one that waited for room) got a machine without the control plane’s own links: no spot notice, no start report, no cache. It has them now.
Two SuperCIs on one computer (the dashboard and a command) could renew the Cloudflare sign-in at the same moment, which ended it. One renews it at a time now, and the other takes what it renewed.
A job no longer lands on a larger machine than it asked for when its first choice has no spot machine. In 0.17.0 the one request for the other pools also held larger sizes, and AWS, going by what it has spare, often gave one of those: a 2-CPU job on a 4-CPU machine, at twice the price. Larger sizes are now asked only after every pool of the size asked for said no.
Not yet: a call of another workflow, a matrix made by an expression. actions/checkout puts the working tree in place as one commit; another repository is not checked out.
When the first choice has no spot machine, the others are asked in one request. Asked one by one, each pool (a machine type in a zone) that had none cost most of a second: a 64-CPU job with fifteen pools waited twelve to fifteen seconds for AWS to say no to all of them. One request for “any of these” answers in about three seconds whatever their number. AWS gives the machine from the pool with the most to spare, so the one least likely to be taken back. That is often an older machine type, and a job is over sooner on a newer one: the pools as fast as the first choice are asked first, and the slower ones in a second request only when none of those has a machine. The first choice is still asked by itself (about a second when it has a machine). A job’s start says so: no spot m8a.16xlarge; r8a.16xlarge in us-east-1d, AWS's choice of 14 pools.
This asks four more permissions of a control plane’s AWS role: to ask for a spot machine from several pools at once, and to make, use and remove launch templates tagged as its own (AWS takes such a request’s machine from a launch template; one is made for the job and removed again). A control plane in AWS gets them when it is updated. One in another cloud that starts AWS machines gets them on the dashboard’s Permissions page or with superci planes allow. Until then pools are asked one by one, as before.
GitHub and GitLab are answered before the machine is asked for (control planes in AWS). They wait ten seconds for an answer and count the event as failed after; with spot machines scarce, asking took longer, and such events showed as failed in superci deliveries list though the job got its machine. The job is claimed, the event answered, and AWS asked right after.
A runner slow to reach GitHub says why. When a machine’s runner takes more than ten seconds to listen for a job, the machine sends what the runner logged meanwhile: each wait of two seconds or more with the line before and after, and its warnings and errors. superci jobs retrieve ID shows it under the job’s start, and --json has it as start.runner_log.
A job cancelled as it is queued gets no machine. GitHub sends a job’s events side by side, and the end of a job cancelled at once (a newer push took its place) can arrive before its start: the start then gave it a machine that sat idle until a sweep. The same for GitLab. A GitLab job cancelled while its machine is being asked for now gets none, or has the one AWS has just given ended at once with its runner removed (before, the machine idled for up to eight minutes).
A job’s record is written only over what was read of it. Requests run side by side (an event, a sweep, a machine), and one could hold a copy for seconds while it waited on AWS and write it back over what another had written meanwhile. A record that ended in between now stays ended, and a machine the older copy got since is ended. The tests cancel a job, start it and end it at every step of a start and of a sweep in turn.
A Cloudflare sign-in that expired no longer stops everything. Signed in to AWS and Cloudflare, with Cloudflare no longer renewing its sign-in, every command that looks for control planes failed, also for a control plane in AWS. Now that sign-in is forgotten and said once (superci planes list adds a line, the pages show a notice with the button to sign in again), and what the other sign-ins reach goes on. What needs Cloudflare says to run superci login cloudflare. Updating a control plane whose runner agent is in Cloudflare updates the control plane and says the agent was left out. The same when Modal no longer takes its token.
Every spot pool is asked before an on-demand machine. One request to the control plane asks twelve pools (a machine type in a zone) at most; a job with more to ask, three families in five zones, went on to an on-demand machine with three pools never asked. It now goes on with the rest a second later, and only then on down your order. A pool that had no machine within the last minute is not asked again, by that job or by another queued beside it: two jobs queued together no longer ask the same ten pools each.
A zone AWS does not offer a machine type in is asked once, then not again for a day (each such request took most of a second, and came before the zone that had the machine).
A machine for a job cancelled before any runner took it is ended at once. Its runner is taken back from GitHub first, which GitHub refuses while a runner is running a job, so a machine that was handed another job a moment before is left alone. Before, it waited for a sweep, up to two minutes on. A job cancelled while its machine is still being asked for gets none, or has the one AWS has just given ended in the same breath: a 64-CPU machine started that way had cost eight cents for nothing. Update your control plane for these.
Labels are written as key=value pairs. runs-on: superci is the default machine. A label that says a machine says it after its name, one key=value for each thing, in any order: runs-on: superci/cpu=8/arch=arm64. The keys: cpu, ram, disk, disk-speed, disk-iops, arch, os, gpu, family, scratch, provider, spot. Numbers have no units (ram=32 is 32 GB). Each thing has one way to be said.
The same keys everywhere. superci machine update takes each of them as a flag (--cpu=8, --spot=false, --scratch=ramdisk, --family=m8a+r8a; --KEY=default leaves one to each provider’s standard again), and the label picker on Workflows writes them. The default machine’s box reads “No spot machines”. --disk-speed takes a number (125 is what AWS includes).
spot is one key: spot=false for a job that must never be on a spot machine, and spot=true in a label undoes a default machine that says never.
scratch is one key, for where what a job writes most is kept: scratch=ramdisk or scratch=localdisk. One or the other. A machine that has a disk of its own now uses it without being asked (a label naming such a family, family=m8id); scratch=localdisk asks for a machine that has one.
family names the AWS machine families a job must have: superci/cpu=32/family=m8a, or several (family=m8a+r8a). Only those are asked for, spot and then on-demand as your order says: for a job that should always have the newest machines, whatever the day’s prices. A family says its architecture (family=m8g is arm64).
What does not go together is said at once, on the job’s page: a key no label has (with the keys there are), a key said twice, a value out of its range, arch=x64 with an arm64 family, scratch=localdisk with a family that has no local disk.
A job’s start is kept, step by step, so a slow one can be seen for what it was. superci jobs retrieve ID says how long each part took: GitHub telling SuperCI of the job, AWS giving a machine (every request and its answer: “no spot m8a.8xlarge in 6 zones; m7a.8xlarge in us-east-1a”), the machine being made, its boot, its disk and Docker, the runner’s files read from the image, the runner reaching GitHub, and GitHub giving it the job. With --json a job has start, with each step in milliseconds. The machine says its own part to the control plane, with a link only it has. Before, a job kept when it was heard of, when its machine was asked for and when it began, and nothing between. Update your control plane for it.
A spot machine is asked for where it costs least for the speed it gives. Every candidate type’s price in every zone is read at once, and pools are asked from the least price up, a newer generation counting as a fifth faster than the one before (a job on it is over sooner). Before, the first type was asked in every zone, then the second: on a day when the newest type had no spot machine anywhere, every job spent seconds learning so, and then took the next in a fixed order, though another cost less. A zone AWS does not offer a type in is not asked for it. On-demand machines are asked for in the types’ own order, as before.
A job whose machine was not the first choice says so in its row: “Not the first choice: AWS had no spot m8a.8xlarge”. Which machine type a job gets changes how long it takes and what it costs, and only the type was shown.
A job says the disk it had. Its row and superci jobs list have the disk’s speed beside its size when it is above what AWS includes (“32 CPU · m7a.8xlarge · 120 GB disk at 1000 MiB/s”), and “RAM disk” or “local disk” when its label asked for one. With --json a job has disk: its size, speed, IOPS, and whether its files were in memory or on a local disk. Before, only the size was shown, and the speed had to be measured inside the job.
A job with scratch=ramdisk or scratch=localdisk keeps the disk speed AWS includes, at no extra price: its writes go to memory or to the local disk, and the faster disk was paid for and barely used. A speed said in the label or on the default machine is still given (superci/cpu=32/scratch=ramdisk/disk-speed=500). Update your control plane for it.
A machine’s disk on AWS is as fast as its size deserves. Every disk had the speed AWS includes, 125 MiB a second, whatever the machine: a 32-CPU machine wrote no faster than a 2-CPU one, and a job that pulls Docker images or runs a database waited for its disk. The speed now follows the machine: 31.25 MiB a second for each CPU, up to 1,000. That is 250 on 8 CPUs, 500 on 16 and 1,000 from 32; the standard 4-CPU machine keeps what AWS includes, and costs what it did. Measured on real machines: sixteen copies of a 300 MB database made in 14 seconds on a 1,000 MiB disk where it took 110, a database’s first start in 3 seconds where it took 26, and 6 GB of Docker images pulled in half the time at twice the speed. Update your control plane for it.
What it costs is in each job’s price: $0.007 an hour on 8 CPUs, $0.021 on 16, $0.055 from 32, a twentieth to a tenth of a spot machine’s price. The default machine’s Disk speed sets it for every job: Standard keeps what AWS includes at no extra price, a number gives every machine that speed (250 MiB a second on the standard machine is $0.007 an hour). In commands: superci machine update --disk-speed=125, =250, or =default to put it back.
A label can say the disk’s speed for one job: superci/cpu=8/disk-speed=1000, and superci/disk-iops=6000 for IOPS (125 to 2000 MiB a second, 3000 to 80000 IOPS; SuperCI keeps them within what a disk of that size takes).
superci/scratch=ramdisk: Docker’s files, the job’s work folder and /tmp in memory, for a job whose many small synced writes (databases, test set-up) no disk speed helps. They count as memory, up to three quarters of the machine’s: ask for it on a machine with memory to spare. AWS, Linux.
superci/scratch=localdisk: a machine with an NVMe disk of its own (AWS’s d families: m8id, c8id, r8id, and m8gd on arm64), with the same folders on it. Ten times the small synced writes of a disk AWS attaches; for writing a lot at once a small machine’s is no faster. Nothing on it outlives the machine. AWS, Linux.
A job on AWS has the disk it was given. The volume had the size asked for (60 GB, or what a label like superci/cpu=8/disk=120 says), but the root filesystem stayed at the image’s 29 GB, 18 of them used: a job had 11 GB free whatever it asked for, and a large one ran out of space. The machine now grows its root filesystem to the whole disk before the runner starts (under a second), on GitHub’s and GitLab’s jobs alike, and the C: drive on Windows. Update your control plane for it; a step that grows the disk by hand is no longer needed.
A job run again after AWS took back its spot machine, in an account whose on-demand limit is too small for it, goes back on a spot machine. Before, it was kept on on-demand, where a machine larger than that limit can never start: it waited and then failed, while the spot limit it had just run under was free. The job says so: “On a spot machine again: an on-demand one does not fit your AWS account’s on-demand limit”.
A job with no spot machine to be had now, whose on-demand machine the account’s limit refuses, waits for a spot machine. Before, it was tried twice more and failed.
A job waiting at the account’s limit said “in use by 1 of your job there; it starts when one ends” when nothing of yours was running: the one counted was a machine that had ended a moment ago, which AWS counts a little longer. It now says that, and is tried again.
A job on an on-demand machine because it was run again after a reclaim says “On-demand: run again after AWS took back its spot machine” (it read “AWS had no spot machine for it”).
A job’s row has its disk beside its machine when it is not the standard 60 GB (“32 CPU · r8a.8xlarge · 120 GB disk”), in superci jobs list too.