Migrating to v0.6.0
v0.6.0 is the biggest shape change since v0.5. It reaches into pipeline YAML, profile YAML, the SDK, the orchestrator's resolution model, and the dispatch surface. Three forces drive the redesign:
- One pipeline = one deployment shape. The v0.5 multi-target pattern (one Go entrypoint with N internal target shapes) is gone. Each pipeline binds an entrypoint to a single shape; multi-environment work becomes multiple pipelines that share an entrypoint.
- Backends are uniform. Secrets, state, cache, and logs are all surfaces of the same
backends.Surfacesbundle. The old separatepkg/sourcespackage is gone; secrets are a fourth surface. - Profiles are named bundles, both in the project and per-user. The project YAML holds project-level profiles;
~/.config/sparkwing/profiles.yamlholds user-level overrides.--profile NAMEreplaces the active bundle wholesale.
Plan on touching every .sparkwing/ repo and ~/.config/sparkwing/profiles.yaml once. Hard cut: there is no v0.5 compatibility runway.
What disappears
| v0.5 | v0.6 |
|---|---|
pipelines[].targets: | Split into N pipelines, each its own shape |
--target <name> flag | Removed; pipeline name is the deployment selector |
sparkwing.Target(ctx) | Removed |
OnTarget("X") on Job/WorkStep/JobGroup | Removed (no compat shim; full deletion) |
Profile default-args: | Removed |
pipelines[].dispatch: block | Removed wholesale (source / requires_approval / protected / backend / runners all gone or relocated) |
pipelines[].secrets: YAML block | Removed; declarations live on the Go Secrets() any provider |
pipelines[].tags, hidden, description (largely) | Removed |
pipelines[].on.manual, on.deploy | Removed |
pipelines[].defaults: | Renamed to pipelines[].args: |
Project YAML profile: (top-level hint) | Moved into defaults.profile |
runners: registry in sparkwing.yaml | Removed; runner selection is per-job Job.Requires(...) labels |
sources: registry in sparkwing.yaml | Removed; secrets are a surface of a profile's backend bundle |
Profile gitcache: field | Removed; CLI discovers via controller GET /api/v1/services |
Profile fields cost_per_runner_hour, auto_allow, default_runner, log_store, artifact_store, detect: | Removed |
Profile flat controller: + token: fields | Now nested: controller: { url, token } |
Profile default: field in profiles.yaml | Removed; no profile is "default" |
| Built-in "laptop" profile fallback | Removed; no profile is active when no --profile is passed |
pkg/sources Go package | Deleted; use pkg/backends.Spec |
PipelineConfig[T], ConfigProvider, ResolvePipelineConfig, WithPipelineConfig, pipelines[].values: | All removed |
Push.target: / Webhook.target: trigger fields | Removed |
Guard tokens profile-local, profile-controller, profile-name:X, git-branch:X | Renamed to profile:local, profile:controller, profile:name=X, git:branch=X |
sparkwing profiles use NAME verb | Removed (no default profile concept) |
Pipeline-level dispatch.runners allowlist | Replaced by pipeline.requires: [labels] |
What's new
defaults:block at the project YAML top level:profile,args,guards,requires. Pipelines inherit; declare your own block to opt out.profiles:map at the project YAML top level: named backend bundles. Same shape as~/.config/sparkwing/profiles.yaml'sprofiles:.backends.Surfaceswith four fields:Secrets,State,Cache,Logs. Every profile must declare all four.type: nonesecrets backend for pipelines that never callSecret(ctx, ...).type: envandtype: filesystemsecrets backends (env vars / dotenv).type: controlleruniversal: secrets/state/cache/logs all express via{type: controller, url, token | token_env}.token_env: VARon any controller-typed spec to source the bearer token from an env var (intended for checked-in project YAML).- Surface inheritance: a controller-typed surface with empty
url/tokeninherits from the profile's top-levelcontroller:block at load time. pipelines[].profile: NAME: per-pipeline profile selector that overridesdefaults.profile.--profilefrom CLI overrides everything.pipelines[].requires: [labels]: pipeline-level runner-label requirements unioned with each job'sJob.Requires(...). The reserved labellocalpins to in-process.defaults.guardsanddefaults.requires: replace wholesale by pipeline-level block when non-empty.- Guard token grammar
namespace:rest:profile:local,profile:controller,profile:name=NAMEgit:branch=NAME,git:branch=defaultarg:FLAG=VALUE
Git.DefaultBranchpopulated from origin's HEAD symref; feedsgit:branch=defaultguard evaluation.RegisterEntrypoint[T](name, factory)+BindPipelinesFromYAML(cfg): one Go entrypoint can back many pipelines; YAML maps name to entrypoint.sparkwing.NewSecretResolverFromSpec(ctx, spec): SDK factory takingbackends.Specdirectly. ReplacesNewSecretResolverFromSource.
Resolution chains (recap)
Profile (wholesale at every layer):
1. --profile NAME (~/.config/sparkwing/profiles.yaml)
2. pipeline.profile NAME (project profiles map)
3. defaults.profile NAME (project profiles map)
4. nil (no profile; orchestrator falls back to sqlite-only test/dev shape)
Args (per-key merge):
schema.Default < schema.Computed < defaults.args < pipeline.args < CLI --flag
Guards / Requires (replace at pipeline level):
pipeline.guards if non-empty, else defaults.guards
pipeline.requires if non-empty, else defaults.requires
YAML before / after
Project YAML
Before (v0.5):
profile: prod # repo hint (top-level)
pipelines:
- name: deploy
entrypoint: Deploy
tags: [deploy, prod]
hidden: false
description: "Push to prod"
on:
push: { branches: [main] }
secrets:
- { name: DEPLOY_TOKEN, required: true }
defaults:
replicas: "10"
dispatch:
source: prod-secrets
runners: [prod-pool]
protected: true
requires_approval: true
targets:
prod: { runners: [prod-pool] }
staging: { runners: [staging-pool] }
runners:
prod-pool:
type: kubernetes
labels: [linux, gpu]
sources:
default: prod-secrets
entries:
prod-secrets:
type: profile
profile: prod
After (v0.6):
defaults:
profile: prod # project-default profile selector
args:
region: us-east
guards:
require: [profile:controller]
requires: [linux]
profiles:
prod:
controller:
url: https://controller.prod.example.com
token_env: PROD_CONTROLLER_TOKEN
secrets: { type: controller }
state: { type: controller }
cache: { type: controller }
logs: { type: controller }
staging:
controller:
url: https://controller.staging.example.com
token_env: STAGING_CONTROLLER_TOKEN
secrets: { type: controller }
state: { type: controller }
cache: { type: controller }
logs: { type: controller }
pipelines:
- name: deploy-prod
entrypoint: Deploy
on:
push: { branches: [main] }
args:
replicas: "10"
- name: deploy-staging
entrypoint: Deploy
profile: staging
args:
replicas: "3"
User profiles.yaml
Before:
default: prod
profiles:
prod:
controller: https://api.example.dev
token: swu_xxx
state: { type: s3, bucket: prod-state }
cache: { type: s3, bucket: prod-cache }
logs: { type: controller }
gitcache: { type: s3, bucket: prod-gitcache }
After:
profiles:
prod:
controller: { url: https://api.example.dev, token: swu_xxx }
secrets: { type: controller }
state: { type: controller }
cache: { type: controller }
logs: { type: controller }
Every profile must declare all four surfaces. Use secrets: { type: none } if you have no secrets backend.
Migration recipe
Per repo:
1. Bump the SDK pin
go get github.com/sparkwing-dev/sparkwing@v0.6.0
go mod tidy
2. Move to sparkwing.yaml if you haven't already
If your repo still has .sparkwing/pipelines.yaml, do v0.5's flatten first:
mv .sparkwing/pipelines.yaml .sparkwing/sparkwing.yaml
Then merge any runners.yaml, sources.yaml, backends.yaml, sparks.yaml content as top-level sections. v0.5's migration guide has details.
3. Trim removed pipeline fields
Remove these keys from every pipelines[] entry: tags, hidden, description (if cosmetic), on.manual, on.deploy, dispatch, secrets. Rename defaults to args. The parser hard-rejects each removed field with a clear "unknown field X" error.
4. Split targets: blocks into N pipelines
For each targets: block, write one pipeline per target. Move per-target runners to requires; source is now via the active profile's secrets: surface; protected is a guards.require: [git:branch=default] token.
5. Move project hints into defaults:
# Was:
profile: prod
# Becomes:
defaults:
profile: prod
6. Add a profiles: map to project YAML (recommended)
profiles:
prod:
controller: { url: https://controller.prod.example.com, token_env: PROD_TOKEN }
secrets: { type: controller }
state: { type: controller }
cache: { type: controller }
logs: { type: controller }
This lets pipelines reference profile: prod without users needing the same name in their personal profiles.yaml.
7. Reshape ~/.config/sparkwing/profiles.yaml
- Drop
default:field. - Wrap each profile's
controller+tokeninto a nestedcontroller: { url, token }. - Remove
gitcache,cost_per_runner_hour,auto_allow,default_runner,log_store,artifact_store,detect:. - Add
secrets:surface (usetype: noneif the profile has no secrets backend). - Make sure every profile declares all four surfaces.
8. Update Go code
- Drop
sparkwing.Target(ctx)calls. - Drop
Job.OnTarget("X")/WorkStep.OnTarget/JobGroup.OnTargetcalls. - Drop
PipelineConfig[T]usages. UseWithArgs[T]with YAMLargs:defaults instead. - Replace
sparkwing.NewSecretResolverFromSource(ctx, src, profileLookup)withsparkwing.NewSecretResolverFromSpec(ctx, spec). - Replace imports of
pkg/sourceswithpkg/backends. The Spec type covers everything Source did, plustype: envandtype: none. - The
Secrets() anyprovider interface still works the same way; the YAMLsecrets:block is gone but the Go-side declaration via struct tags + reflection persists.
9. Update guard tokens
profile-local -> profile:local
profile-controller -> profile:controller
profile-name:NAME -> profile:name=NAME
git-branch:NAME -> git:branch=NAME
git-branch:default -> git:branch=default
arg:FLAG=VALUE -> (unchanged)
10. Run pre-commit and pre-push gates
sparkwing run pre-commit
sparkwing run pre-push
Fix any errors that surface.
What didn't change
Plan/Job/Work/Step/Spawnprimitives.Job.Requires(...),Job.Prefers(...),Job.WhenRunner(...)runner labels.Job.Inline,Job.Optional,Job.ContinueOnError, retry / timeout modifiers.RunAndAwaitcross-pipeline awaits.- Triggers:
push,webhook,schedule,pre_commit,pre_push. - The
Secrets() anyprovider interface. PipelineSecrets[T](ctx)typed accessor.sparkwing.Secret(ctx, "NAME")runtime lookup.Plan.Spawn(...)dynamic child-job creation.
Common gotchas
- "profile NAME: secrets surface is required" -- every profile must declare all four surfaces. Add
secrets: { type: none }for profiles that don't need them. - "defaults.profile NAME is not declared in profiles:" -- the named profile must exist in the project's
profiles:map. - "pipeline NAME: profile NAME is not declared in project profiles:" -- same, but for per-pipeline overrides.
secrets:block at pipeline level errors with "unknown field" -- gone in v0.6. Move declarations to the Go-sideSecrets() anyprovider.--targetflag rejected -- gone. Pipeline name is the selector.
Worked example: split a multi-target deploy
Before (one pipeline, multiple targets):
pipelines:
- name: deploy
entrypoint: Deploy
defaults: { replicas: "5" }
dispatch: { source: prod-secrets }
targets:
prod:
defaults: { replicas: "10" }
dispatch: { runners: [prod-pool], protected: true }
staging:
defaults: { replicas: "3" }
dispatch: { runners: [staging-pool] }
After (two pipelines, one entrypoint):
defaults:
profile: prod
pipelines:
- name: deploy-prod
entrypoint: Deploy
args:
replicas: "10"
requires: [prod-pool]
guards:
require: [profile:controller, git:branch=default]
- name: deploy-staging
entrypoint: Deploy
profile: staging
args:
replicas: "3"
requires: [staging-pool]
Operators run sparkwing run deploy-prod or sparkwing run deploy-staging. Each pipeline gets its own --help page, its own flag set, its own policy.