Blueprints
A Blueprint defines a repository's build as a graph of Tasks. Workshop runs it as a Job. Each Task runs one command on its own disposable VM.
How a Job runs
- A push, pull request, schedule or user starts a Job at a pinned commit.
- Provision runs first. It checks out the commit, builds the Blueprint project, runs it to produce a plan, and freezes the source.
- Each Task starts on a new VM with a copy of the frozen source once its dependencies have succeeded.
- The Job ends when every Task has settled.
Consequences:
- Tasks share no filesystem. Exchange files through artifacts.
- Frozen source has no
.githistory.WORKSHOP_REVISIONholds the commit. - A retry reuses the frozen source. New code needs a new Job.
- A new head or base on a pull request cancels that pull request's running Job.
- The Workshop owner configures triggers. A repository cannot.
Create a Blueprint
Workshop reads .workshop/blueprints.toml from the default branch. Provision builds the project at the Job's commit.
# .workshop/blueprints.toml
[[blueprints]]
key = "ci"
name = "CI"
project = ".workshop/Blueprints.csproj"
<!-- .workshop/Blueprints.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="CerebeWorkshop.Protocol"><HintPath>$(WorkshopSdkPath)</HintPath></Reference>
</ItemGroup>
</Project>
// .workshop/Program.cs
using System.Text.Json;
using CerebeWorkshop.Protocol;
if (args is not ["ci", var output]) throw new ArgumentException("Expected <key> <plan-path>.");
var plan = new BlueprintPlan([
new("checks", "Lint, type-check and unit test", [], new("bash", [".workshop/tasks/checks.sh"])),
new("integration", "Integration tests", ["checks"], new("bash", [".workshop/tasks/integration.sh"]), WorkerSize: "heavy")
], WorkerSize: "medium");
plan.Ordered(); // Throws on an invalid plan.
await File.WriteAllTextAsync(output, JsonSerializer.Serialize(plan, Wire.Json));
Provision calls the program with <key> <plan-path>. The program must handle every declared key. Commands run from the repository root unless CommandSpec.Directory says otherwise.
Limits
| Limit | Value |
|---|---|
| Tasks per Job | 100, excluding Provision. Target 30 or fewer |
| Task run time | 60 minutes, including source restore |
| Retained output | Latest 40,000 characters per Task |
| Artifact size | 128 MiB stored, 512 MiB expanded |
| Plan size | 128,000 characters of JSON |
| Task ID | 63 characters: lowercase letters, digits and hyphens, starting with a letter. provision is reserved |
| Task name | 100 characters |
| Blueprints per repository | 100 |
blueprints.toml |
64 KiB |
| Inputs per Blueprint | 8 |
Workers
| Size | vCPU | Memory |
|---|---|---|
light |
2 | 4 GiB |
medium |
2 | 8 GiB |
heavy |
4 | 16 GiB |
BlueprintPlan.WorkerSizesets the default, which islight.TaskSpec.WorkerSizeoverrides it. Provision must fitlight.lightandmediumhave the same CPU. Choosemediumfor memory andheavyfor CPU.- Size from measurements.
get_jobreturns each Task's peak memory, CPU time and OOM kills. Work inside containers under-reports memory. - Workers run Linux by default.
WorkerOsselectswindowsfor a plan or Task, andprovisionOsin the TOML does so for Provision. - The Linux image includes Docker, Make, Git, jq, .NET 10, and pinned Go, Node, Helm, yq and uv under
/opt/workshop-tools./usr/local/share/workshop/container-image-toolchains.txtlists them. A Task installs anything else itself. Nothing is cached between Tasks.
Design Tasks
Every Task costs a VM boot, a source restore, tool setup and a worker slot. Bundle as many operations as form one coherent unit of work into one Task.
Split a Task only when:
- Its parts run long enough that parallel execution shortens the Job.
- Its parts need different worker sizes, operating systems or secrets.
- A part must fail or retry independently.
Tasks can fail, and a Task fails and retries as a whole. Design for that:
- Run every check in a bundled Task, then exit non-zero if any failed. One run then reports every failure.
- Do not bundle long, unrelated work. A retry repeats all of it.
- Make every Task safe to run again.
Keep the graph small:
- Target 30 Tasks or fewer. A plan near the limit of 100 is a design problem.
- Shard long suites by measured duration, not by file count.
- Cap computed shard counts so the largest plan fits. Check full runs on the default branch, which usually plan the most Tasks.
- Declare a dependency only when a Task needs another Task's output or success.
When Tasks fail
| Event | Task state | Effect on the Job |
|---|---|---|
| Command exits non-zero | Failed |
Dependents are skipped. Other Tasks finish. The Job ends Failed |
Failure with AllowFailure: true |
Failed |
Dependents continue. The Job can still succeed |
| Worker lost or stopped | Interrupted |
Dependents are skipped. Other Tasks finish. The Job ends Interrupted unless a required Task failed |
| Provision does not succeed | Failed or Interrupted |
The Job ends. No other Task can run |
RunAfterFailure: truestarts a Task after its dependencies finish, even if they failed. Use it for report collectors.FailFast: trueon the plan ends the Job at the first required failure.- Nothing retries automatically.
retry_tasksruns a finished Job's unsuccessful Tasks and everything downstream of them again.
Share artifacts
using var job = JobContext.FromEnvironment();
await job.StoreArtifact("build-output", "work/publish"); // File or folder.
// In a Task that depends on the producer:
await job.GetArtifact("build-output", "work/restored"); // The destination must not exist.
- Declare a dependency on the producer.
GetArtifactfails on a missing artifact and does not wait. - Names are unique within a Job, and a stored artifact cannot change. A retry removes what the retried Task stored.
- Stored artifacts do not expire.
- A Task reads artifacts from its own Job only.
- Other languages use the SDK through a small command in the Blueprint project:
dotnet run --project .workshop/Blueprints.csproj -- <action>. list_artifactsand the Job page expose stored artifacts after the Job.
Read logs
- Workers stream each Task's stdout and stderr live to the Job page.
- Workshop keeps only the latest 40,000 characters per Task. It is not a durable log.
- Print failures and a summary last, so they survive.
- Store full logs as an artifact when they matter.
- Flush output. A buffering program looks silent until it flushes.
get_task_outputreads the retained text, up to 16,000 characters per call. Useoffsetto page.
Set priority
[[blueprints]]
key = "deploy"
name = "Deploy"
project = ".workshop/Blueprints.csproj"
priority = 10
priorityis a whole number from0to255. Lower runs first. The default is100.- Waiting work runs in order of priority, then creation time. This covers every ready Task, including Tasks of running Jobs, across all of a client's repositories.
- Lower-priority work waits for as long as higher-priority work is ready. Nothing ages.
- Change priority only for a specific need. If every Blueprint is high priority, none is.
- A new value applies after it merges to the default branch.
Use inputs and secrets
[[blueprints]]
key = "publish"
name = "Publish"
project = ".workshop/Blueprints.csproj"
[[blueprints.inputs]]
name = "services"
label = "Services"
choices = ["backend", "portal"]
defaults = ["backend", "portal"]
multiple = true
JobContext.InputsFromEnvironment()reads the selected values. They are frozen when the Job starts. Never use inputs for credentials.SecretProfileon a Task requests a secret profile. The Workshop owner must approve it. Pull request Jobs and Provision never receive secrets.
Every Task receives:
| Variable | Value |
|---|---|
WORKSHOP_JOB_ID, WORKSHOP_TASK_ID |
Job and Task IDs |
WORKSHOP_REVISION |
The commit under test |
WORKSHOP_PULL_REQUEST_NUMBER |
Pull request number, or empty |
WORKSHOP_BASE_REVISION, WORKSHOP_HEAD_REVISION |
Pull request commits, or empty |
WORKSHOP_LAST_SUCCEEDED_REVISION |
The branch's last successful commit, or empty |
WORKSHOP_INPUTS_JSON |
Selected inputs |
Diagnose a failed Job
Most failures belong to the repository. Workshop ran the command and reported its result. Fix the Blueprint, the Task or the code, and do not expect Workshop to change.
get_jobshows each Task's state and usage. Find the first Task that did not succeed.get_task_outputshows why it stopped.- Use this table to decide who acts.
| Symptom | Owner | Action |
|---|---|---|
Task Failed |
Repository | Fix the code, test or script |
| Test passes on retry | Repository | Fix the flaky test |
Provision Failed with a build error or exception |
Repository | Fix the Blueprint project |
A Blueprint must contain 1–100 Tasks |
Repository | Bundle Tasks or cap shards |
| OOM kills, or peak memory near the total | Repository | Use a larger worker, or do less in the Task |
Interrupted with Execution deadline reached |
Repository | Split the Task or make it faster |
| Artifact missing, or its name conflicts | Repository | Add the dependency, or use a unique name |
| Tool not found | Repository | Install it in the Task |
Task Interrupted with worker lost in the Job |
Workshop | Retry. Report it if it recurs across Jobs |
Worker terminated before claiming this Task |
Workshop | Retry. Report it if it recurs across Jobs |
Tasks stay Queued with no higher-priority work waiting |
Workshop | Report it |
| No Job appears for a commit | Workshop owner | Check the trigger configuration |
Report a Workshop problem with the Job URL, the Task ID and the relevant output.