Migrating to v0.67.0
Rewritten CLI
Use the command reference shipped with the binary for the current grammar. The archived notes below describe the CLI before the rewrite and are historical.
- Run a configured pipeline with
sparkwing run deploy-prod. Author arguments follow--. - Inspect a pipeline with
sparkwing pipeline describe --pipeline deploy-prodor preview it withsparkwing pipeline plan --pipeline deploy-prod. Inspection acquires the selected pipeline source and uses its verified engine. - Read saved connection details with
sparkwing profiles show --profile prod. It prints configuration and the token prefix. Check authenticated controller access withsparkwing runners list --profile prod. - Update the CLI with
sparkwing update. Upgrade the project's SDK pin withsparkwing upgrade. The old update target selectors have been removed. - Manage timers with
sparkwing schedules list. Installed timers invoke the private tick entrypoint automatically.
Before: archived pre-rewrite notes
These notes are retained as a historical migration record. Their commands and behavior do not describe the rewritten CLI. Use the instructions above.
Deployment selection uses pipeline names
The restored run --target TARGET flag is removed. Select a configured pipeline
for each deployment shape, as in v0.6.0. For example, invoke
sparkwing run deploy-prod for the configured deploy-prod pipeline. An author
argument named target follows -- and belongs to the pipeline.
The sparkwing-full chart now refuses to render a controller that would start
unauthenticated. Installs that already name a bootstrap admin token Secret need
nothing; every other install sets one value on upgrade.
sparkwing-full requires a bootstrap admin token
-
Before:
controller.requireAuthdefaulted tofalseandcontroller.bootstrapAdminToken.nameto empty. A fresh install served every controller route, includingPOST /api/v1/tokensand first-admin creation, unauthenticated to anything that reached the Service until a token was minted and the controller restarted. -
After:
controller.requireAuthdefaults totrue, and the render fails with an error naming both options whilecontroller.bootstrapAdminToken.nameis empty andcontroller.allowOpenBootstrapis nottrue. -
Upgrade, recommended: create a Secret holding a token-shaped value and name it. The controller ignores it once a live token exists, and
--require-authkeeps refusing to start if the tokens table is ever emptied:# The umask makes the file 0600: it holds an admin bearer. (umask 077 && printf 'swu_%s' "$(openssl rand -hex 24)" > "$HOME/sparkwing-bootstrap-admin") kubectl -n sparkwing create secret generic sparkwing-bootstrap-admin \ --from-file=token="$HOME/sparkwing-bootstrap-admin" helm upgrade sparkwing ./charts/sparkwing-full --namespace sparkwing -f my-values.yaml \ --set controller.bootstrapAdminToken.name=sparkwing-bootstrap-admin -
Upgrade, keeping the previous shape: set
controller.allowOpenBootstrap=true. The chart then omits--require-auth. A controller that already holds a live token stays authenticated; one whose tokens table is empty serves every route unauthenticated until a token is minted and it restarts, as before. The value must be a YAML bool; a quoted string fails the render. -
Edge cases:
controller.requireAuth=falsealso renders without a bootstrap token and keeps the old behavior. Values files that setrequireAuth: truewithout a bootstrap token, which crash-looped a fresh install, now fail at render time instead.
The OIDC subject gains a repository segment, so every cloud trust policy that matches sub must be rewritten before the controller is upgraded, or the roles it guards refuse Sparkwing tokens.
OIDC subject names the repository
The sub of a controller-issued ID token now carries GitHub's numeric repository id between the team and the pipeline. Before, two repositories of one team that subscribed the same pipeline name minted the same subject, so a push to main of either repository satisfied a trust policy meant for one of them.
- Before:
team:acme:pipeline:deploy:trigger:push:runner:runner:ref:refs/heads/main - After:
team:acme:repository_id:123456789:pipeline:deploy:trigger:push:runner:runner:ref:refs/heads/main
Every trust policy, federated credential or role that matches sub must be updated:
- Find the repository id:
gh api repos/<owner>/<name> --jq .id. - Insert
repository_id:<id>:afterteam:<team>:in every AWSStringEqualsorStringLikecondition on<issuer>:sub, every Entra federated credentialsubject, and every Vaultbound_subjector glob onsub. - A token minted before the upgrade carries the old subject and lives at most one token lifetime (10 minutes by default), so to avoid a gap, admit both forms during the rollout and drop the old one afterwards. In AWS that is a list of both values under
StringLike; in Entra, a second federated credential.
AWS trust policy, before:
"StringLike": {"api.sparkwing.dev:sub": "team:acme:pipeline:deploy:trigger:push:runner:runner:ref:refs/heads/main"}
After:
"StringLike": {"api.sparkwing.dev:sub": "team:acme:repository_id:123456789:pipeline:deploy:trigger:push:runner:runner:ref:refs/heads/main"}
Edge cases:
- The segment is set only when a GitHub App delivery started the run, or when a retry or child run inherits that repository. Runs from the CLI, the API or a schedule carry an empty segment,
team:acme:repository_id::pipeline:.... A policy for such runs insertsrepository_id::. - A policy that ends in
*after an earlier segment, such asteam:acme:*, keeps matching, and now also matches every repository. Narrow it toteam:acme:repository_id:<id>:*when the role belongs to one repository. - Google Cloud and Vault examples that condition on the individual claims keep working, but they should add a condition on
repository, because a pipeline name is not unique across a team's repositories. See OIDC tokens for cloud roles.
Go callers of internal/store update one call site; nothing else changes on a
running install.
SettleCardPayment returns the matched warning
Store.SettleCardPayment gains a middle return, a *store.WarnedCardPayment
that is nil unless an actionable early fraud warning matched the payment or
the card that paid. It names the warning, whether the payment settled the
open charge as a repayment (Repays), the debt it repaid and the rest the
ledger does not hold. A caller alerts on it, because a warned payment
either repays a pay-now debt with the team held or grants nothing, and any
unapplied rest is the operator's to return.
- Before:
created, err := st.SettleCardPayment(ctx, payment, now) - After:
created, warned, err := st.SettleCardPayment(ctx, payment, now)
A redelivery of a settled payment returns nil, so an alert keyed on it fires once per payment.
The cache service keeps a run's cache entries apart by repository and git ref. Nothing needs configuring; expect more cache misses on new branches at first.
Cache entries follow the repository and ref
Before: every run of a team read and wrote one shared set of /cache,
/bin and /artifacts entries in the cache service, so a pull request's run
could replace the dependency archive or binary a later main run restored.
After: the cache grant a run receives carries its repository and refs, read by the controller from the run's trigger. The run writes only under its own ref and reads, in order, its own ref, its pull request's base branch, the repository's default branch, and finally the entries written before this release.
A run whose ref and commit its submitter chose writes under a ref of its own, so it reads what its branch's pushes wrote but never replaces it. That covers runs started from the CLI, the API and the dashboard, and retries or child runs that descend from one of them. Runs whose ref the server holds write under the real ref and warm the caches pushes read: runs a signed GitHub webhook started, scheduled runs that follow a branch tip, and retries or child runs of either that keep its ref and commit and upload no source. A schedule pinned to a commit counts as submitter-chosen, because any editor can pin one to a commit that is not on the branch.
What you see: the first run on a new branch restores its base branch's cache as before. A branch's writes stay on that branch, so a sibling branch or another repository of the team no longer finds them and rebuilds. Artifacts a node hands to later nodes are content-addressed and stay shared by the team, so a memoized or retried node still stages them on any ref. Entries from before the upgrade stay readable by every run until a run writes the same key under its own ref. The cache refuses a grant minted by an older controller, so upgrade the controller before or with the cache; see Cache grants need a claim and a scope.
Why: a deploy role that trusts ref:refs/heads/main relies on branch
protection deciding what runs on main; a shared cache let a branch's run put
code into a later main run.
A deployment that still starts sparkwing-runner worker switches to the
combined runner before upgrading; the chart and the documented manifests never
ran it.
sparkwing-runner worker is removed
-
Before:
sparkwing-runner worker --controller <url> [--runner inprocess|k8s|warm]polled the trigger queue and ran each claimed trigger, either in-process or by sending its nodes to Kubernetes Jobs or the warm pool. -
After: the
workersubcommand is unknown and exits 2. The combined runner is the only trigger-claiming loop insparkwing-runner. -
Upgrade: replace the container args:
sparkwing-runner worker --controller=URL --logs=URL --runner=k8s \ --namespace=NS --image=IMAGE --runner-sa=SA --image-pull-secret=SECRETbecomes
sparkwing-runner runner --controller=URL --logs=URL --also-claim-triggers --claim-nodes=false \ --trigger-runner=k8s --trigger-runner-namespace=NS --trigger-runner-image=IMAGE \ --trigger-runner-sa=SA --trigger-runner-image-pull-secret=SECRET--trigger-sources,--metrics-addrand--dependency-proxycarry over, and--tokenbecomes the fileagent-tokenunder--credentials-dir;--image-pull-policy,--kubeconfig,--runner-controller-urland--runner-logs-urlgain the--trigger-runner-prefix, and--artifact-storebecomes--trigger-artifact-store. Drop--claim-nodes=falseto let the same pod also run nodes. The worker's--log-storehas no runner equivalent: the runner streams logs to--logs.--k8s-cpu-ceiling,--k8s-memory-ceilingand--k8s-job-deadlinebecome--cpu-ceiling,--memory-ceilingand--deadline. -
Before:
sparkwing cluster workerwas an in-process claim loop for a profile. -
After: self-hosted runners register to Cloud with
sparkwing runners add --runner NAME --profile PROFILE.
sparkwing-logs reads flags only
-
Before: every
sparkwing-logsflag took its default from an environment variable of the same meaning:SPARKWING_CONTROLLER_URL,SPARKWING_REQUIRE_AUTH,SPARKWING_LOGS_ARCHIVE_STORE,SPARKWING_LOGS_ARCHIVE_IDLE, theSPARKWING_LOGS_*limits, andSPARKWING_LOGS_EGRESS_*. -
After: the service reads none of them. A variable left in the pod's environment is ignored, so the setting it carried falls back to the flag's default. A malformed flag value stops the service at startup with an error naming the flag.
-
Operator steps: the
sparkwing-runner-bundlechart already passes flags and needs nothing. In a manifest of your own, move each variable to its flag:Variable Flag SPARKWING_CONTROLLER_URL--controllerSPARKWING_REQUIRE_AUTH--require-authSPARKWING_LOGS_ARCHIVE_STORE--archive-storeSPARKWING_LOGS_ARCHIVE_IDLE--archive-idleSPARKWING_LOGS_MAX_NODE_BYTES--max-node-bytesSPARKWING_LOGS_MAX_RUN_BYTES--max-run-bytesSPARKWING_LOGS_MAX_INFLIGHT_BYTES--max-inflight-bytesSPARKWING_LOGS_MIN_FREE_BYTES--min-free-bytesSPARKWING_LOGS_RETENTION--retentionSPARKWING_LOGS_SWEEP_INTERVAL--sweep-intervalSPARKWING_LOGS_SEARCH_MAX_BYTES--search-max-bytesSPARKWING_LOGS_SEARCH_TIMEOUT--search-timeoutSPARKWING_LOGS_MAX_LINE_BYTES--max-line-bytesSPARKWING_LOGS_BINARY_RATIO--binary-ratioSPARKWING_LOGS_MAX_STORE_BYTES--max-store-bytesSPARKWING_LOGS_MAX_STORE_OBJECTS--max-store-objectsSPARKWING_LOGS_WARN_STORE_BYTES--warn-store-bytesSPARKWING_LOGS_WARN_STORE_OBJECTS--warn-store-objectsSPARKWING_LOGS_STORE_RECONCILE--store-reconcileSPARKWING_LOGS_EGRESS_DAILY_ALARM_BYTES--egress-daily-alarm-bytesSPARKWING_LOGS_EGRESS_MAX_DOWNLOADS--egress-max-downloadsSPARKWING_LOGS_EGRESS_MAX_LOG_STREAMS--egress-max-log-streams -
Edge cases:
SPARKWING_REQUIRE_AUTHalso acceptedyesandon;--require-authis a boolean flag, so pass it bare. A retention set throughSPARKWING_LOGS_RETENTIONused to count as named and kept an archived service off its 90-day default; only--retentioncounts now.SPARKWING_S3_ENDPOINTgave way toAWS_ENDPOINT_URL_S3; see below.
sparkwing-cache reads flags and a credentials directory
-
Before:
sparkwing-cachetook each flag's default from an environment variable, including eleven without theSPARKWING_prefix, and its two secrets fromSPARKWING_API_TOKENandSPARKWING_CACHE_GRANT_KEYor the--api-tokenand--grant-keyflags. A value it could not parse was ignored: a malformed duration or boolean kept the default without a word, and a malformed byte count printed a warning and stayed unlimited. -
After: the cache reads settings from flags alone and its secrets from
--credentials-dir, a directory holding one file per secret:cache-token(the operator token) andcache-grant-key. An absent file turns that feature off, as an unset variable did.--api-tokenand--grant-keyare gone, because a flag value shows in/proc/<pid>/cmdline. A malformed flag value now stops the cache at startup with an error naming the flag; so does a--credentials-dirthat names no directory. -
Operator steps: the
sparkwing-runner-bundlechart projectscache.tokenSecretandcache.grantKeySecretinto/etc/sparkwing/credentialsand passes the flags; upgrading the chart needs no value changes. In a manifest of your own, mount the two Secret keys as files and move each variable to its flag:args: - --credentials-dir - /etc/sparkwing/credentials volumeMounts: - name: credentials mountPath: /etc/sparkwing/credentials readOnly: true volumes: - name: credentials projected: defaultMode: 0400 sources: - secret: name: sparkwing-cache-token items: [{key: token, path: cache-token}] - secret: name: sparkwing-cache-grant-key items: [{key: key, path: cache-grant-key}]Variable or flag Use instead SPARKWING_API_TOKEN,--api-tokenthe file cache-tokenunder--credentials-dirSPARKWING_CACHE_GRANT_KEY,--grant-keythe file cache-grant-keyunder--credentials-dirPORT,PORT_ADDR--addrDATA_DIR--data-dirPROXY_CACHE_DIR--proxy-cache-dirPROXY_CACHE_TTL--proxy-cache-ttlPROXY_MAX_AGE--proxy-max-ageFETCH_INTERVAL--fetch-intervalFETCH_FRESH_WINDOW--fetch-fresh-windowRECLONE_COOLDOWN--reclone-cooldownGITCACHE_REPOS--auto-register-reposSSH_KEY_DIR--ssh-key-dirSPARKWING_CONTROLLER_URL--controllerSPARKWING_METRICS_ADDR--metrics-addrSPARKWING_CACHE_PUBLIC_URL--public-urlSPARKWING_CACHE_TRUST_FORWARDED_HOST--trust-forwarded-hostSPARKWING_CACHE_ALLOW_UNAUTHENTICATED--allow-unauthenticatedSPARKWING_CACHE_BLOB_STORE--blob-storeSPARKWING_CACHE_MAX_ARCHIVE_BYTES--max-cache-archive-bytesSPARKWING_CACHE_MAX_STORE_BYTES--max-store-bytesSPARKWING_CACHE_MAX_STORE_OBJECTS--max-store-objectsSPARKWING_CACHE_WARN_STORE_BYTES--warn-store-bytesSPARKWING_CACHE_WARN_STORE_OBJECTS--warn-store-objectsSPARKWING_CACHE_STORE_RECONCILE--store-reconcileSPARKWING_CACHE_PROXY_MAX_BYTES--proxy-max-bytesSPARKWING_CACHE_EGRESS_DAILY_ALARM_BYTES--egress-daily-alarm-bytes -
Edge cases: a cache whose environment carried a malformed value used to start on the default; with the value moved to its flag it refuses to start, so check the value before the upgrade.
--allow-unauthenticatedis a boolean flag: pass it bare, because--allow-unauthenticated=yesis refused. The controller still reads the same token asSPARKWING_CACHE_TOKENand the grant key asSPARKWING_CACHE_GRANT_KEY; when it moves to a credentials directory it reads the samecache-tokenandcache-grant-keyfile names, so one projected Secret volume will serve both.SPARKWING_S3_ENDPOINT,SPARKWING_LOG_FORMATandSPARKWING_LOG_LEVELchanged too; see the sections below.
sparkwing-runner reads flags and a credentials directory
-
Before:
sparkwing-runner runnerandsparkwing-runner launchtook their bearer fromSPARKWING_AGENT_TOKENor--token, andrunnerseeded most flags fromSPARKWING_*variables. The Kubernetes Job ceilings, deadline and team-node switch existed only asSPARKWING_K8S_CPU_CEILING,SPARKWING_K8S_MEMORY_CEILING,SPARKWING_K8S_JOB_DEADLINEandSPARKWING_RUNNER_TEAM_NODESon the runner pod, which each trigger'shandle-triggerchild inherited and read. -
After: both commands read their bearer from the file
agent-tokenunder--credentials-dir;--tokenis gone, because a flag value shows in/proc/<pid>/cmdline. The runner takes--cpu-ceiling,--memory-ceiling,--deadlineand--team-nodes, the launcher's names, validates them at startup, and hands them to each trigger ashandle-triggerflags.handle-triggerno longer reads those four variables, norSPARKWING_RUNNER_SA,SPARKWING_IMAGE_PULL_POLICYandSPARKWING_DEPENDENCY_PROXY_URL, which the runner already passed as flags.POD_NAME,POD_NAMESPACE,KUBECONFIGand the GitHub Actions variables are still read, because other tools define them. The token a Job or a trigger child receives from Sparkwing still crosses inSPARKWING_AGENT_TOKEN; that is Sparkwing's own protocol, not a setting. -
Operator steps: the
sparkwing-runner-bundlechart projectscontroller.tokenSecretasagent-tokenand passesrunner.jobCeilingas flags; upgrading needs no value changes. A Helm user who setSPARKWING_K8S_JOB_DEADLINEorSPARKWING_RUNNER_TEAM_NODESthroughrunner.extraEnvmoves them to the new valuesrunner.jobDeadlineandrunner.teamNodes. An external gitcache moves from aSPARKWING_GITCACHE_URLentry inrunner.extraEnvtorunner.gitcacheUrl. In a manifest of your own, mount the token Secret as a file and move each variable to its flag:Variable or flag Use instead SPARKWING_AGENT_TOKEN,--tokenthe file agent-tokenunder--credentials-dirSPARKWING_K8S_CPU_CEILINGsparkwing-runner runner --cpu-ceilingSPARKWING_K8S_MEMORY_CEILING--memory-ceilingSPARKWING_K8S_JOB_DEADLINE--deadlineSPARKWING_RUNNER_TEAM_NODES--team-nodesSPARKWING_CONTROLLER_URL--controllerSPARKWING_LOGS_URL--logsSPARKWING_GITCACHE_URL--gitcacheSPARKWING_RUNNER_SA--trigger-runner-saSPARKWING_CACHE_URL--trigger-artifact-storeSPARKWING_DEPENDENCY_PROXY_URL--dependency-proxySPARKWING_IMAGE_PULL_POLICY--trigger-runner-image-pull-policySPARKWING_WARM_MODULES--warm-modulesSPARKWING_LOCAL_RESERVE--local-reserveSPARKWING_TEAM--teamA machine connected from the dashboard runs the new command the machines page prints, which writes the token to
$HOME/.config/sparkwing/runner-credentials/agent-tokenwith mode0600before it starts the runner. -
Edge cases:
--team-nodesreaches a trigger ashandle-trigger --runner-team-nodes, which a pipeline built from an SDK older than v0.66.0 rejects; the runner passes it only when set. A pipeline built from an SDK older than this release still reads the four Job variables if they remain in the runner pod's environment, so delete them rather than leaving them beside the flags.sparkwing-runner agent, which readsconfig.yaml, is unchanged.
sparkwing-controller reads its secrets from a credentials directory
-
Before: the controller took each secret from an environment variable, some also from a
*-fileflag or a*_FILEvariable naming a path, with three different precedence rules between the two forms. -
After: every secret is one file in
--credentials-dir, under a fixed name. There is no other way in. An absent file leaves its feature off, as an unset variable did. The six*-fileflags are gone, so a controller started with one exits with an unknown-flag error. The variables below are no longer read. -
Operator steps: the
sparkwing-fullchart projectscontroller.databaseSecret,controller.secretsKey,controller.secretsPreviousKey,controller.bootstrapAdminTokenand the runner bundle's cache token and grant key into/etc/sparkwing/credentialsand passes--credentials-dir; those values need no change. Secrets the chart had no value for, which a manifest passed throughcontroller.extraEnv, go into one Secret named by the newcontroller.credentialsSecret.name, keyed by file name. For example:kubectl -n sparkwing create secret generic sparkwing-controller-credentials \ --from-file=license=license.txt \ --from-file=github-app-key=app.pem \ --from-literal=github-app-webhook-secret="$WEBHOOK_SECRET" \ --from-literal=github-client-secret="$GITHUB_CLIENT_SECRET" helm upgrade sparkwing ./charts/sparkwing-full -f my-values.yaml \ --set controller.credentialsSecret.name=sparkwing-controller-credentialsOutside Kubernetes, write each value to its file under a directory only the controller's user can read, and pass
--credentials-dir.Variable or flag Credential file SPARKWING_PG_URLpg-urlSPARKWING_SECRETS_KEY,--secrets-key-filesecrets-keySPARKWING_SECRETS_PREVIOUS_KEY,--secrets-previous-key-filesecrets-key.previousSPARKWING_BOOTSTRAP_ADMIN_TOKEN,--bootstrap-admin-token-filebootstrap-admin-tokenSPARKWING_LICENSE,--license-filelicenseSPARKWING_OIDC_KEY,--oidc-key-fileoidc-keySPARKWING_OIDC_PUBLISHED_KEY,--oidc-published-key-fileoidc-key.publishedSPARKWING_GOOGLE_CLIENT_SECRETgoogle-client-secretSPARKWING_GITHUB_CLIENT_SECRETgithub-client-secretSPARKWING_GITHUB_APP_PRIVATE_KEY,SPARKWING_GITHUB_APP_PRIVATE_KEY_FILEgithub-app-keySPARKWING_GITHUB_APP_WEBHOOK_SECRETgithub-app-webhook-secretSPARKWING_CLOUDFRONT_PRIVATE_KEY,SPARKWING_CLOUDFRONT_PRIVATE_KEY_FILEcloudfront-keySPARKWING_LOGS_DELETE_TOKENlogs-delete-tokenSPARKWING_BILLING_TOKENbilling-tokenSPARKWING_CACHE_TOKENcache-tokenSPARKWING_CACHE_GRANT_KEYcache-grant-key -
Edge cases: a
--credentials-dirthat names no directory stops the controller, and so does any credential file that is present but empty or only whitespace, naming the file: an emptypg-urlwould otherwise select SQLite and an emptybootstrap-admin-tokenwould leave the API open. An absent file still leaves that credential unset. ThesparkwingCLI still readsSPARKWING_SECRETS_KEY,SPARKWING_SECRETS_PREVIOUS_KEYandSPARKWING_CACHE_TOKENon a laptop; only the controller stopped.sparkwing-controller migrate-outputstakes--credentials-dirtoo, for itspg-url.
The controller never stores a secret as plaintext
- Before: a single-team controller started without a secrets key stored every secret value as plaintext and logged a warning.
- After: no secret is stored as plaintext. Without the
secrets-keycredential, a multi-team controller refuses to start, as before. A single-team SQLite controller creates a key on the first secret write and keeps it insecrets.keybesidestate.db, mode0600. A single-team PostgreSQL controller starts and refuses each secret write with409naming the file to add. A server built frominternal/controllerwith no cipher refuses secret reads and writes with503. - Operator steps: a SQLite controller needs nothing, but back up
secrets.keywithstate.dbfrom now on. A PostgreSQL controller that stores secrets needs a key: create one withopenssl rand -base64 32, put it in thesecrets-keycredential (chart:controller.secretsKey), and restart. - Edge cases: rows an older controller stored as plaintext are sealed
at startup. A SQLite controller creates its key to do so; a PostgreSQL
controller without a key refuses to start while any plaintext row remains,
and says how many. A SQLite store that holds sealed values but has lost
its
secrets.keyrefuses to start rather than create a second key.
sparkwing-controller reads settings from flags only
-
Before: most controller flags took their default from an environment variable, and a few settings existed only as variables: the CloudFront domain and key pair id, the object-store request budgets, the breaker switch and its trip reset.
-
After: the controller reads no configuration variable. Every setting is a flag; a malformed value stops it at startup naming the flag. Two settings gained a flag,
--cloudfront-domainand--cloudfront-key-pair-id, and the eight budget variables became one flag,--object-store-budget.SPARKWING_OBJECT_STORE_BREAKERandSPARKWING_OBJECT_STORE_TRIP_RESETare gone without a replacement: nothing set them, the breaker stays on, a day trip clears when its day rolls, andsparkwing operator object-store reset-breakerclears one sooner. The budget variables are no longer read by any process: a localsparkwing run, the cache and the logs service run on the built-in budgets. -
Operator steps: the
sparkwing-fullchart now passes--dashboard-urland--cache-urlas flags. A chart install that set controller settings throughcontroller.extraEnvmoves each to its value:controller.extraEnvnameChart value SPARKWING_EXTERNAL_URLcontroller.externalURLSPARKWING_OAUTH_REDIRECT_URIScontroller.oauthRedirectURIsSPARKWING_OPERATOR_ACCOUNTScontroller.operatorAccountsSPARKWING_GITHUB_CLIENT_IDcontroller.github.clientIDSPARKWING_GITHUB_APP_IDcontroller.github.appIDSPARKWING_GITHUB_APP_SLUGcontroller.github.appSlugSPARKWING_GOOGLE_CLIENT_IDcontroller.google.clientIDSPARKWING_EMAIL_SENDERcontroller.email.senderSPARKWING_EMAIL_CONFIGURATION_SETcontroller.email.configurationSetSPARKWING_CLOUDFRONT_DOMAINcontroller.cloudfront.domainSPARKWING_CLOUDFRONT_KEY_PAIR_IDcontroller.cloudfront.keyPairIDany other name in the table below its flag in controller.extraArgs, such as--billing-url=https://...A secret set through
controller.extraEnv(SPARKWING_GITHUB_APP_PRIVATE_KEY,SPARKWING_GITHUB_CLIENT_SECRETand the rest) becomes a key ofcontroller.credentialsSecretnamed after its credential file; see the section on the credentials directory above. A manifest of your own moves each variable to its flag:Variable Flag SPARKWING_METRICS_ADDR--metrics-addrCACHE_POD_URL--cache-pod-urlSPARKWING_LOGS_URL--logs-urlSPARKWING_DASHBOARD_URL--dashboard-urlSPARKWING_BILLING_URL--billing-urlSPARKWING_OPERATOR_ACCOUNTS--operator-accountsSPARKWING_CACHE_URL--cache-urlSPARKWING_EXTERNAL_URL--external-urlSPARKWING_DEFAULT_PREFER_LABELS--default-prefer-labelsSPARKWING_OBJECT_STORE_URL--bucket-storeSPARKWING_OBJECT_STORE_MAX_BUCKET_BYTES--max-bucket-bytesSPARKWING_OBJECT_STORE_MAX_BUCKET_OBJECTS--max-bucket-objectsSPARKWING_OBJECT_STORE_WARN_BUCKET_BYTES--warn-bucket-bytesSPARKWING_OBJECT_STORE_WARN_BUCKET_OBJECTS--warn-bucket-objectsSPARKWING_OBJECT_STORE_BUCKET_RECONCILE--bucket-reconcileSPARKWING_OBJECT_STORE_BUCKET_MEASURE_PAGES--bucket-measure-pagesSPARKWING_OBJECT_STORE_<CLASS>_PER_<WINDOW>--object-store-budget class:window=count,...SPARKWING_CACHE_BLOB_STORE--cache-blob-store(also onmigrate-outputs)SPARKWING_LOGS_ARCHIVE_STORE--logs-archive-storeSPARKWING_CONTROLLER_EGRESS_DAILY_ALARM_BYTES--egress-daily-alarm-bytesSPARKWING_CONTROLLER_EGRESS_MAX_DOWNLOADS--egress-max-downloadsSPARKWING_CONTROLLER_EGRESS_MAX_LOG_STREAMS--egress-max-log-streamsSPARKWING_GOOGLE_CLIENT_ID--google-client-idSPARKWING_GITHUB_CLIENT_ID--github-client-idSPARKWING_GITHUB_APP_ID--github-app-idSPARKWING_GITHUB_APP_SLUG--github-app-slugSPARKWING_OAUTH_REDIRECT_URIS--oauth-redirect-urisSPARKWING_EMAIL_SENDER--email-senderSPARKWING_EMAIL_CONFIGURATION_SET--email-configuration-setSPARKWING_REQUIRE_AUTH--require-auth(chart:controller.requireAuth)SPARKWING_CLOUDFRONT_DOMAIN--cloudfront-domainSPARKWING_CLOUDFRONT_KEY_PAIR_ID--cloudfront-key-pair-id -
Edge cases:
SPARKWING_REQUIRE_AUTHacceptedyesandon;--require-authis a boolean flag, so pass it bare. ASPARKWING_OBJECT_STORE_BUCKET_MEASURE_PAGESthe controller could not read used to print a warning and keep the default;--bucket-measure-pagesrefuses it. Ininternal/egress,Bindtakes no environment.
AWS_ENDPOINT_URL_S3 replaces SPARKWING_S3_ENDPOINT
- Before:
SPARKWING_S3_ENDPOINTpointed every S3 client Sparkwing builds at an S3-compatible store and addressed buckets path-style. - After: the AWS SDK's own
AWS_ENDPOINT_URL_S3, orAWS_ENDPOINT_URLfor every AWS service, names the endpoint; the SDK reads it. Sparkwing addresses buckets path-style whenever either is set.SPARKWING_S3_ENDPOINTis no longer read by any process. - Operator steps: rename the variable to
AWS_ENDPOINT_URL_S3in every environment that set it: a runner, a laptop shell, a CI job, and the controller, cache and logs Deployments. - Edge cases: a detached run and a remote node child carry both names
from the submitting environment, as they carried
SPARKWING_S3_ENDPOINT.AWS_ENDPOINT_URLalso redirects every other AWS client in the process, such as SES invitation email on the controller; use the S3-specific name unless that is what you want.
Services take --log-format and --log-level
- Before:
sparkwing-cachealone readSPARKWING_LOG_FORMAT=jsonandSPARKWING_LOG_LEVELand wrote its log to stdout; the other services wrote text at info level to stderr whatever was set. - After: the controller, cache, logs service, runner and launcher take
--log-format text|jsonand--log-level debug|info|warn|error, write to stderr, and stop at startup on a value they do not know. No service reads the two variables; thesparkwingCLI and the pipeline binary still do. - Operator steps: replace
SPARKWING_LOG_FORMAT=jsonandSPARKWING_LOG_LEVELon a cache Deployment with the flags. A log shipper that read the cache's stdout reads its stderr instead.
Machine settings move from the environment to config.yaml
-
Before: the
sparkwingCLI, the admission daemon and the pipeline binary read these machine settings from the environment:Variable config.yaml key SPARKWING_BUDGETadmission.budget(already read; the variable no longer overrides it)SPARKWING_CACHE_MAX_BYTEScache.max_bytesSPARKWING_CACHE_MAX_ENTRIEScache.max_entriesSPARKWING_NO_AUTO_REGISTER=1repos.auto_register: falseSPARKWING_AUTO_REGISTER_WORKTREES=1repos.include_worktrees: trueSPARKWING_BOX_IDmachine.box_idSPARKWING_LOGS_DROP_POLICY=warnlogs.drop_policy: warnSPARKWING_PAUSE_TIMEOUTdebug.pause_timeoutSPARKWING_SUBMIT_ENV_ALLOW(comma-separated)run.submit_env_allow(a YAML list)SPARKWING_RERUN_IMAGErerun_imageon the profiledebug rerun --profilenamesSPARKWING_VERSION_HOLDoverrode the version-hold file for one shell. -
After: none of those variables is read. Each setting lives in config.yaml and holds for every process on the machine. The version hold is the file
sparkwing version hold --set VERSIONwrites, alone.sparkwing cache prune --max-bytes SIZEand--max-entriesstill override the cache ceilings for one prune, anddebug rerun --imagestill overrides the profile's image. -
Operator steps: move each exported value into the matching key, for example:
cache: max_bytes: 4GiB machine: box_id: rack-3-host-7 run: submit_env_allow: [AWS_PROFILE, AWS_REGION, DOCKER_*]A shell that exported
SPARKWING_VERSION_HOLDrunssparkwing version hold --set <version>instead. -
Edge cases:
cache.max_bytes,cache.max_entriesandrun.submit_env_allowrefuse a value they cannot parse, naming the file and key; the variables fell back to the default silently. A misspelled key in any section fails the read. A container that setSPARKWING_BOX_IDpointsSPARKWING_CONFIGat a file carryingmachine.box_id. The detached run snapshot no longer carriesSPARKWING_SUBMIT_ENV_ALLOWitself.SPARKWING_ADMISSION_CLASSstays: a hook sets it for the run it starts, and it goes withAdmissionClass.
One-off run switches become --sw-* flags
-
Before: five environment variables switched
sparkwing runbehaviour for one invocation:SPARKWING_NO_BINCACHE,SPARKWING_NO_SPARKS_RESOLVE,SPARKWING_HASH_ALL_FILES,SPARKWING_ARTIFACT_DIGEST_BACKFILLandSPARKWING_ALLOW_UNADMITTED. -
After:
sparkwing run PIPELINEtakes the flags instead:Variable Flag SPARKWING_NO_BINCACHE=1--sw-no-bincacheSPARKWING_NO_SPARKS_RESOLVE=1--sw-no-update, which already skipped sparks resolutionSPARKWING_HASH_ALL_FILES=1--sw-hash-all-filesSPARKWING_ARTIFACT_DIGEST_BACKFILL=1--sw-artifact-digest-backfillSPARKWING_ALLOW_UNADMITTED=1--sw-allow-unadmittedThese environment variables are no longer read. Local, detached, scheduled and Cloud runs retain their selected build policy in the sealed request. Remote inspection and hosting use that same policy.
--sw-allow-unadmittedselects an exclusive local controller for CI and refuses detached execution. -
Operator steps: replace each exported variable with its flag on the
sparkwing run PIPELINEcommand line. Use--before author arguments.
The remaining laptop variables are removed
-
Before:
SPARKWING_DISPATCH_WAIT_TIMEOUTbounded a run's wait for a dispatch;SPARKWING_NAMESPACEnamed the namespace forsparkwing debug attach; the admission daemon readTYPESAFE_API_KEYfrom its environment;SPARKWING_STORE_WEDGE_BUDGET,SPARKWING_SQLITE_BUSY_TIMEOUT_MSandSPARKWING_DOCS_BASE_URLtuned internals for tests; the run-node and trigger paths fell back to$SPARKWING_HOME/dev.envforSPARKWING_CONTROLLER_URL,SPARKWING_LOGS_URLandSPARKWING_CACHE_URL, andSPARKWING_DEV_ENV_DISABLEclosed that fallback. -
After:
Before After SPARKWING_DISPATCH_WAIT_TIMEOUTrun.dispatch_waitin config.yaml (a duration, oroff)SPARKWING_NAMESPACEsparkwing debug attach --run RUN --job JOB --namespace NAME(defaultsparkwing)TYPESAFE_API_KEYin the daemon's environmentremoved with the Jev admission mode SPARKWING_STORE_WEDGE_BUDGET,SPARKWING_SQLITE_BUSY_TIMEOUT_MS,SPARKWING_DOCS_BASE_URLremoved; the values are fixed, and tests set them in code the dev.env fallback on the run-node and trigger paths removed; those paths read their own environment only SPARKWING_DEV_ENV_DISABLEremoved -
Operator steps: move an exported dispatch wait into
run.dispatch_wait; pass--namespacetodebug attach; remove the Jev admission mode and its configuration fields. -
Edge cases: an unparseable
run.dispatch_waitstops the run at startup, naming the file and key; the variable fell back to the default with a warning.internal/store.BusyTimeoutEnvVarandpkg/docs.BaseURLEnvVarare removed;store.SetTestBusyTimeoutanddocs.SetTestBaseURLreplace them for tests. A CLI verb still finds the local dashboard through dev.env.SPARKWING_WINGD_BINis removed. The local controller lifecycle receives its executable path directly.
Leftover variable names are removed
- Before:
SPARKWING_GITCACHEnamed a gitcache for the SDK's clone helper ahead ofSPARKWING_GITCACHE_URL. - After: the clone helper reads
SPARKWING_GITCACHE_URLalone. - Author or operator steps: rename
SPARKWING_GITCACHEtoSPARKWING_GITCACHE_URL. - Edge cases:
SPARKWING_TOKEN,SPARKWING_TRIGGER_CLAIM_GENERATION,SPARKWING_TRIGGER_GENERATIONandSPARKWING_ATTEMPT_ORDINALwere stripped from child environments although nothing set or read them; they no longer appear in the code.
Flag-mirror environment variables are no longer read
Each of these variables only supplied a flag's default. Neither chart sets them; a custom manifest that does passes the flag instead.
| Variable | Use instead |
|---|---|
SPARKWING_TRIGGER_RUNNER | sparkwing-runner runner --trigger-runner |
SPARKWING_RUNNER_IMAGE | --trigger-runner-image |
SPARKWING_IMAGE_PULL_SECRET | --trigger-runner-image-pull-secret |
SPARKWING_RUNNER_CONTROLLER_URL | --trigger-runner-controller-url |
SPARKWING_RUNNER_LOGS_URL | --trigger-runner-logs-url |
SPARKWING_RUNNER_NODE_SELECTOR | --trigger-runner-node-selector, once per entry |
SPARKWING_RUNNER_TOLERATION | --trigger-runner-toleration, once per entry |
SPARKWING_GITCACHE_CONCURRENCY | sparkwing-cache --git-fork-limit |
SPARKWING_WINGD_VERSION | sparkwing wingd run --version VERSION |
SPARKWING_FORCE_COLOR=1 | CLICOLOR_FORCE=1 |
SPARKWING_BAKED_BINARY named a pipeline binary inside the runner image for
triggers without a repository. The runner image ships no such binary, so the
fallback only failed later with no such file; such a trigger now fails at
once saying it has no repository. Delete the variable from any runner
manifest. SPARKWING_CHAOS_KEEP kept chaos-test homes and has no replacement;
a failed chaos test still keeps its home.
Edge case: a variable set on the runner pod also reached each trigger's
handle-trigger child, which read the same names. Both reads are gone, so
the flag on sparkwing-runner runner is the only way to set these values.
Per-pipeline GitHub webhooks are gone, so every repository that posted to
/webhooks/github/<pipeline> must move to the GitHub App before the
controller is upgraded, or its pushes and pull requests stop starting runs.
Per-pipeline GitHub webhooks are removed
- Before: a repository webhook posted to
POST /webhooks/github/{pipeline}, signed with a secret fromsparkwing cluster webhooks connect, theGITHUB_WEBHOOK_BINDINGSdocument orGITHUB_WEBHOOK_SECRET, and the controller'sGITHUB_TOKENposted commit statuses for its pull request runs. - After: the GitHub App is the only signed GitHub
trigger. The old
/webhooks/github/{pipeline}route, the/api/v1/webhooks/github/bindingsAPI (POSTto connect,DELETEto disconnect) and its table, the commands (Before:sparkwing cluster webhooks), the three environment settings and the chart valuescontroller.githubWebhookSecretandcontroller.githubStatusTokenare gone. The OpenAPI document drops the bindings API's bodies,components.schemas.GitHubWebhookBindingRequest,components.schemas.GitHubWebhookBindingResponseandcomponents.schemas.GitHubWebhookDisconnectResponse. A delivery to the old URL answers404. Schema v92 drops the stored bindings and their secrets; nothing reads them back.
To move a repository:
-
Configure the App on the controller, once per deployment, as Operator configuration lists:
SPARKWING_GITHUB_APP_ID,SPARKWING_GITHUB_APP_SLUG,SPARKWING_GITHUB_APP_PRIVATE_KEY_FILE(or_PRIVATE_KEY) andSPARKWING_GITHUB_APP_WEBHOOK_SECRET, with the App's webhook pointed athttps://<controller>/webhooks/github-app. Thesparkwing-fullchart takes these throughcontroller.extraEnv, and its ingress does not route/webhooks/github-app, so add a rule for it as you did for the old path. The hosted service already runs the App. -
A team owner opens Team -> GitHub, chooses Connect GitHub, and installs the App on the account that owns the repository (Connecting a team).
-
Subscribe each pipeline the old webhook URL named, in the same tab or with the API. The old webhook ran
pushandpull_request(opened,synchronize,reopened) and ignored tag pushes; this subscription keeps that, andbranchesnarrows it:curl -sS -X PUT https://<controller>/api/v1/team/github-app/triggers \ -H "Authorization: Bearer $SPARKWING_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"repository":"your-org/my-app","pipeline":"build-deploy","push":true,"pull_request":true}' -
Delete the old webhook on GitHub (repository Settings -> Webhooks), or leave it to fail with
404. -
Remove
GITHUB_WEBHOOK_SECRET,GITHUB_WEBHOOK_BINDINGSandGITHUB_TOKENfrom the controller's environment, andcontroller.githubWebhookSecretandcontroller.githubStatusTokenfrom Helm values. The controller ignores the environment variables, and thesparkwing-fullchart refuses to render while either value still names a Secret.
Edge cases:
- Runs the App starts report a check run named
sparkwing/<pipeline>instead of a commit status under the same name. A branch protection rule that requiressparkwing/<pipeline>and pins it to a source expects the old token's statuses; set the App as that check's source. An installation whose owner has not accepted the Checks permission gets commit statuses until it does. - App runs carry the repository id in the OIDC subject, so a trust policy
written for webhook runs with
repository_id::must name the id (OIDC subject names the repository). - A run already queued from the old webhook still runs. If it mints an OIDC
token after the upgrade, its trigger is
trigger:manual, because it carries no repository id. - A subscription runs only for repositories in the team's installation, and a pull request from a fork starts nothing, as before.
Legacy cache routes are removed
Before: sparkwing-cache served source archives and single files
(/archive, /file, /tree-hash, /branch-contains), tarball uploads
(/upload, /uploads/<id>), ancestor negotiation and bundle seeding
(/sync/negotiate, /sync/seed), an eager mirror fetch (/git/refresh) and
job-ID artifacts (/artifacts/<job>). The controller proxied seeding and
refresh as POST on /api/v1/gitcache/seed and /api/v1/gitcache/refresh.
All of them took the cache's operator token or admin, except
/artifacts/<job>, which also took a run's cache grant.
After: those routes answer 404. The cache keeps /git/register,
/git/<name>/..., /bin/..., /cache/..., /admin/teams/{team},
/admin/store-ceiling/..., /health, /metrics, /stats and /proxy/....
The controller keeps POST /api/v1/gitcache/git/register and the clone proxies.
What replaces each:
| Removed | Use instead |
|---|---|
/archive, /file, /tree-hash, /branch-contains | Clone /git/<name> with SPARKWING_CACHE_TOKEN and read the checkout. |
/git/refresh, POST /api/v1/gitcache/refresh | Nothing: a clone refreshes a mirror older than FETCH_FRESH_WINDOW before it answers. |
/sync/seed, POST /api/v1/gitcache/seed, /sync/negotiate, /upload, /uploads/<id> | Give the cache a credential for origin and register the repository, or run a runner without --gitcache so it fetches from origin itself. --working-tree already uploads to the direct data store. |
/artifacts/<job> | Declare Outputs and Consumes; the SDK stores artifacts under content-addressed /bin/artifacts/... keys or the direct data store. |
Operator steps:
- Remove
--max-artifact-bytesand--workspace-seed-max-agefrom the cache's arguments before upgrading; a cache started with either exits.SPARKWING_CACHE_MAX_ARTIFACT_BYTESandWORKSPACE_SEED_MAX_AGEare ignored. With the runner-bundle chart, dropcache.limits.maxArtifactBytes. - Drop dashboards and alerts on
sparkwing.gitcache.archives_served,sparkwing.gitcache.files_servedandsparkwing.gitcache.recovery_reclones. - Optionally reclaim space:
<data-dir>/archives/,<data-dir>/uploads/,<data-dir>/artifacts/and each<data-dir>/teams/<team>/artifacts/(orteams/<team>/artifacts/andteams/<team>/scopes/*/artifacts/under--blob-store) hold nothing the cache reads any more, and the store ceiling no longer counts the first three. Mirrors may still carryrefs/sparkwing-workspace/*andrefs/sparkwing-workspace-archive/*refs from working-tree seeds; nothing prunes them now, and deleting them withgit update-ref -dinside the mirror is safe.
Why: these routes were a second way into the shared mirrors and the volume, kept for flows the CLI and runners no longer use, and each needed its own review.
Cache grants need a claim and a scope
Before: the cache honored any grant its key signed. A grant with no claim and no repository scope, which controllers minted before grants carried a scope, read and wrote the team's whole cache tree until it expired.
After: the cache and the controller's data routes refuse, with 401, a grant that names no claim or no repository scope. Every grant a current controller mints names both.
Upgrade: upgrade the controller before the cache, or both together. A run holding a grant from an older controller loses the cache, and a runner that asks for a fresh grant from an upgraded controller gets one; nothing else needs changing.
Why: the grant is the cache's only team boundary, and a grant that named neither the claim nor the repository it was minted for outlived both.
doctor no longer reports box-slot locks
- Before:
sparkwing doctorread$SPARKWING_HOME/box-slots, reported a live holder as a pipeline pinned before v0.16, and purged idle lock files.sparkwing queueprinted the same warning. - After: neither command reads the directory.
doctor -o jsonno longer carrieslegacy_box_slot_files_removedorlive_legacy_holders, and-o plainno longer prints those two rows. - Upgrade: scripts that read either field drop it. A leftover
$SPARKWING_HOME/box-slotsdirectory holds only lock files and can be deleted withrm -r "${SPARKWING_HOME:-$HOME/.sparkwing}/box-slots".
Unused pkg functions are removed
Go programs that import these pkg/ functions call the replacement instead:
| Removed | Use instead |
|---|---|
controller.Serve(ctx, st, addr, logger) | controller.ServeWith(ctx, controller.New(st, logger), addr) |
logs.Serve, logs.ServeWithTokens, logs.ServePrivateWithTokens | logs.ServeWith(ctx, logs.ServeOptions{Root, Addr, ControllerURL, Logger, Private}) |
sparkwinglogs.FromClient(c) | sparkwinglogs.New with the client's URL and token |
store.DetectDialect(dsn) | store.Open for a SQLite path, store.OpenPostgres for a postgres:// DSN |
controller.AuditFields, store.SetArgon2AcquireTimeout, backends.LayerSurfaces | nothing; no caller used them |
backends.LayerSurfaces layered a pipeline target's backend over the
profile's, a block sparkwing.yaml no longer accepts.
Unimplemented backend types are removed
Code that imports TypeGCS, TypeAzureBlob or TypeMySQL from
github.com/sparkwing-dev/sparkwing/pkg/backends no longer compiles; remove
the reference, since no build ever opened a backend of those types. Nothing
changes at run time: a profile naming gcs, azure-blob or mysql failed at
run start before and still does, now with an error naming the types its
surface accepts.
The warm-PVC pool is removed
Before: sparkwing-controller --pool kept a set of PVCs labeled
sparkwing.dev/pool=cache warm with privileged docker:27-dind pods and
served GET on /api/v1/pool and POST on /api/v1/pool/checkout,
/api/v1/pool/return and /api/v1/pool/heartbeat.
sparkwing-full turned it on by default (controller.pool.enabled: true) and
gave the controller a Role over PVCs, pods, configmaps, events and namespaces,
a ClusterRole over StorageClasses, and a warmer ServiceAccount.
After: the controller has no pool and makes no Kubernetes API calls. The
four routes answer 404, and the OpenAPI document drops the list response,
components.schemas.PoolListResponse. Nothing Sparkwing ships ever checked a PVC out, so no
build loses a cache it was using.
Operator steps:
- Remove
--pool,--pool-namespace,--warmer-service-accountand--kubeconfigfrom the controller's arguments before upgrading; a controller started with any of them exits.SPARKWING_WARMER_SAandPOD_NAMESPACEare no longer read. - With
sparkwing-full, dropcontroller.pool.*andrbac.*from your values. The upgrade deletes the controller's Role, RoleBinding, ClusterRole, ClusterRoleBinding and<release>-sparkwing-full-cache-warmerServiceAccount, and the controller pod no longer mounts an API token. Annotations onserviceAccountfor IRSA or Workload Identity keep working. - Outside the chart, delete the Role and ClusterRole you granted the
controller for the pool, and any
sparkwing-cache-warmerServiceAccount. - Reclaim the pool's storage once the new controller runs:
kubectl delete pod,pvc -n <pool-namespace> -l sparkwing.dev/managed=pool-manager. Thesparkwing-cache-configConfigMap, if you created one, is no longer read. - Drop dashboards and alerts on
sparkwing.pool.reconcile_duration,sparkwing.pool.warm_duration,sparkwing.pool.checkoutsandsparkwing.pool.returns.
Why: the pool held the controller's broadest Kubernetes rights and ran the only privileged pods Sparkwing created, and no runner, launcher or chart template ever mounted one of its PVCs.
The concurrency notify stream is removed
Before: GET /api/v1/concurrency/{key}/notify?run_id=...&node_id=...
held a server-sent event stream open for up to 30 minutes and sent one
ready, superseded or stream_end event when the waiter resolved.
After: /api/v1/concurrency/{key}/notify answers 404. Nothing Sparkwing
ships opened it.
Upgrade: a client of your own that waited on the stream polls
GET /api/v1/concurrency/{key}/resolve?run_id=...&node_id=... instead. It
takes the node's claim token or runs.state rather than runs.read, and its
status field carries the same outcomes.
Why: an unused stream that a claim token could hold open for half an hour was attack surface with no caller.
The controller artifact route is removed
Before: a controller or local daemon given an artifact store served
GET /api/v1/artifacts/{key} to a runs.read caller, run keys to the run's
team and content-addressed keys to the operator.
After: /api/v1/artifacts/{key} answers 404. Nothing Sparkwing ships
called it.
Upgrade: a script that read artifacts this way reads the artifact store
its profile names directly, the local directory or the bucket. On a cluster
with direct data storage, POST /api/v1/data/download returns a signed URL
for an artifacts/... key.
Why: a second read path into the shared artifact store needed its own team check and its own egress accounting, and served no caller.
Four operator routes are removed
Before: an admin token could read the controller's egress meter with its
top consumers (GET /api/v1/egress), read a named team's credit balance
(GET /api/v1/credits/teams/{team}), and give a team a free-tier slot past
--free-team-slots (PUT /api/v1/storage/teams/{team}/free-slot). The cache's
operator token could list its mirror files with GET /repos.
After: /api/v1/egress, /api/v1/credits/teams/{team},
/api/v1/storage/teams/{team}/free-slot and the cache's /repos answer 404.
What replaces each:
| Removed | Use instead |
|---|---|
GET /api/v1/egress | sparkwing_egress_day_bytes and sparkwing_egress_daily_alarm on /metrics; /api/v1/health reports the alarm. Per-principal totals are no longer served. |
GET /api/v1/credits/teams/{team} | The operator console's GET /api/v1/operator/teams/{team}, or GET /api/v1/credits with the team's own credential. |
PUT /api/v1/storage/teams/{team}/free-slot | Raise --free-team-slots so the team takes a slot with its first byte. |
cache GET /repos | List <data-dir>/repos/ on the cache volume. |
Why: each was a privileged route with no caller, and each needed its own review as the route table moves to one declared list.
CLI aliases and internal verbs
Every verb the CLI dispatches is now a registered command, so --help,
sparkwing commands and completion see the whole surface.
| Before | After |
|---|---|
sparkwing configure profiles ls | sparkwing profiles list |
sparkwing configure profiles rm NAME, ... delete NAME | sparkwing profiles remove --profile NAME --keep-token |
sparkwing configure profiles dup ... | none; see Connection verbs fold into cloud |
sparkwing secrets rm ..., sparkwing secrets remove ... | sparkwing secrets delete --secret NAME |
sparkwing pipeline sparks ls, ... rm ... | sparkwing pipeline sparks list, ... remove ... |
sparkwing configure xrepo ls, ... rm ... | sparkwing repos list --checkouts, sparkwing repos remove ... |
sparkwing runs consumer kill | none; the consumer command is removed |
sparkwing run <pipeline> config [-o json] | sparkwing pipeline describe --pipeline <pipeline> --secrets [-o json] |
sparkwing pipeline publish | none; a runner compiles the pipeline on first use and shares the binary through the cache |
sparkwing run-node | none; the pipeline binary and sparkwing-runner run-node keep the node protocol |
-
Shell completion: the nine
_complete-*helpers are one hiddensparkwing __complete --kind KIND. A completion script installed by an earlier release calls the old names and completes nothing; regenerate it:sparkwing completion --shell zsh > "${fpath[1]}/_sparkwing" -
Timer tick: Before:
sparkwing crons tick. The internal command is nowsparkwing __tick. Runsparkwing schedules installto refresh installed timers. Custom schedulers must call the new internal command.
cluster gc, examples scaffold and retired-flag pointers are removed
| Before | After |
|---|---|
sparkwing cluster gc [--root DIR] [--profile P] | none; sparkwing-runner runner sweeps its warm root when it starts |
sparkwing examples scaffold --name EXAMPLE | sparkwing examples --example EXAMPLE --body, then save the body under .sparkwing/jobs/ |
--on P, --sw-on P | --profile P |
sparkwing run X --sw-profile P | sparkwing pipeline trigger X --profile P |
--sw-target T | Select the pipeline by name: sparkwing run T |
sparkwing run X --sw-isolated-home DIR | SPARKWING_HOME=DIR sparkwing run X |
The retired spellings still fail; the error now says the flag is unknown instead of naming its replacement.
Root flags replace per-verb directory and home flags
-C DIR goes before the verb and applies to every verb.
| Before | After |
|---|---|
sparkwing run X --sw-cd DIR, sparkwing run X -C DIR | sparkwing -C DIR run X |
sparkwing pipeline list --sw-cd DIR (and every other verb that took --sw-cd) | sparkwing -C DIR pipeline list |
sparkwing pipeline sparks list --sparkwing-dir DIR/.sparkwing | sparkwing -C DIR pipeline sparks list |
sparkwing cache explain --dir DIR/.sparkwing | sparkwing -C DIR cache explain |
sparkwing pipeline hooks install --repo DIR (also uninstall, status, fire) | sparkwing -C DIR pipeline hooks install |
sparkwing crons install --repo DIR (also uninstall) | sparkwing -C DIR schedules install |
sparkwing daemon status --home DIR (also queue, doctor, serve *, runs cancel/retry/bounce, runs consumer *) | SPARKWING_HOME=DIR sparkwing daemon status |
-Cwalks up from DIR to the nearest.sparkwing/, so it accepts any directory inside the repository.--sparkwing-dirandcache explain --dirnamed the.sparkwing/directory itself; pass its parent to-C.pipeline lint --dirstill names the pipeline source to scan.--repostill names a registered repository onrepos, and anOWNER/NAMEfilter onrunsandclusterverbs.--profile NAMEmay also go before the verb, for verbs that accept it. Without it, every verb acts on the local store; see The default profile is removed.runaccepts--profileonly for an S3 or Postgres state profile; see Local execution and controller profiles.
The default profile is removed
A command reaches a controller only when --profile names it on that
invocation. Neither the project config nor the environment selects a
profile, so a script or shell history shows every remote it touches.
| Before | After |
|---|---|
defaults: {profile: prod} in .sparkwing/sparkwing.yaml | Remove the key and pass --profile prod to each command that should reach prod. The key fails the config load with field profile not found in type projectconfig.Defaults. |
sparkwing cloud connect ... --set-default | sparkwing profiles add --profile prod --controller URL, then --profile NAME on each command |
export SPARKWING_PROFILE=prod; sparkwing runs list | sparkwing --profile prod runs list |
sparkwing runs ... in a pipeline step, reading the profile the run used | Pass --profile P to that call; without it the call reads the local store |
SPARKWING_CONTROLLER_URL=URL sparkwing runs cancel --run ID | sparkwing runs cancel --run ID --profile NAME, with NAME a profile for URL |
- Every verb that accepts
--profile(runs,serve,pipeline trigger,debug,secrets,crons,cluster, and similar) acts on the local store without it.runs cancel,retry,bounceandprunewithout--profilereach only the local controller, which starts automatically. crons install --profile Pinstalls the schedules on P's controller; without it the schedules arm this machine's timer. A cron or wrapper script that reached a controller throughdefaults.profileor an exportedSPARKWING_PROFILEacts on the local store. Reinstall it with the profile named:sparkwing schedules install --profile prod.
Local execution and controller profiles
Before: sparkwing run PIPELINE --profile NAME opened shared S3 or PostgreSQL
state directly. Local execution now uses sparkwing run PIPELINE and its local
controller. Submit remote runs with
sparkwing pipeline trigger PIPELINE --profile NAME. Keep PostgreSQL as the
controller's database, rather than a profile's local run backend.
Runs verbs fold into status, list and logs
| Before | After |
|---|---|
sparkwing runs get --run ID | sparkwing runs status --run ID -o json --exit-zero |
sparkwing runs wait --run ID [--timeout 10m] [--poll 3s] | sparkwing runs status --run ID --follow --timeout 10m [--poll 3s] |
sparkwing runs summary --run ID | sparkwing runs status --run ID --view summary --exit-zero |
sparkwing runs timeline --run ID [--steps] [--width N] | sparkwing runs status --run ID --view timeline [--steps] [--width 80] --exit-zero |
sparkwing runs receipt --run ID | sparkwing runs status --run ID --view receipt --exit-zero |
sparkwing runs errors ID | sparkwing runs status --run ID --view errors --exit-zero |
sparkwing runs tree --run ID | sparkwing runs status --run ID --view tree --exit-zero |
sparkwing runs last [--pipeline P] [--watch] | sparkwing runs list --limit 1 [--pipeline P] [--follow] |
sparkwing runs find --git-sha S --repo R --root-only --wait --find-timeout D | sparkwing runs list --sha S --repo R --root-only --wait --wait-timeout 10m |
sparkwing runs failures [--group-by step|node] | sparkwing runs list --status failed --group-by run|step|node |
sparkwing runs grep --pattern P [filters] | sparkwing runs logs --grep P |
sparkwing runs triggers list|get | sparkwing runs list |
- Exit codes:
runs statusexits 1 for a run that did not succeed, and every view inherits that; add--exit-zerowhere a script read a view of a failed run.--follow --timeoutkeepsruns wait's codes: 0 succeeded, 1 failed or cancelled, 2 timed out, 3 the run could not be read. - Output shape:
runs wait -o jsonprinted the run record;runs status -o jsonprints{"run": ..., "nodes": ...}, so read.run.statusinstead of.status.runs get -o jsonoutput is a subset ofruns status -o json. - Defaults:
runs findlooked back one hour by default;runs listhas no default lookback, so pass--since 1hto keep that window.runs failuresprinted one row per failed run; that is--group-by run. - Profiles: the replacements read the local store unless
--profileis given, asruns get,wait,last,find,failures,grepand the views did.
Pipeline verbs fold into list, plan and hooks status
| Before | After |
|---|---|
sparkwing pipeline run X [flags] | sparkwing run X [flags] |
sparkwing pipeline discover --query Q [-o json] | sparkwing pipeline list --search Q [-o json] |
sparkwing pipeline explain --name X [-- pipeline-flags] | sparkwing pipeline plan --pipeline X [-- pipeline-flags] |
sparkwing pipeline explain --all | List with sparkwing pipeline list, then plan each pipeline with sparkwing pipeline plan --pipeline X |
sparkwing pipeline hooks fire [--fleet] | Removed; use sparkwing pipeline hooks status to inspect installed content |
sparkwing pipeline hooks survey [--ungated] | Removed; inspect each repository with sparkwing pipeline hooks status |
- Output, exit codes and JSON shapes are unchanged; only the invocation moves.
- A CI step that gates on
pipeline explain --allchanges its command line and nothing else. - Pipelines scaffolded by an earlier
pipeline newlist Before:sparkwing pipeline explain --name Xamong their examples; edit the example tosparkwing pipeline plan --pipeline X.
Crons verbs fold into uninstall, list, show and set
| Before | After |
|---|---|
sparkwing crons disarm NAME | sparkwing schedules uninstall --schedule NAME |
sparkwing crons status | sparkwing schedules list --timer |
sparkwing crons next [--count N] | sparkwing schedules list --next 3 |
sparkwing crons next NAME [--count N] | sparkwing schedules show --schedule NAME --next 3 |
sparkwing crons lock NAME | sparkwing schedules set --schedule NAME --pin |
sparkwing crons unlock NAME | sparkwing schedules set --schedule NAME --unpin |
sparkwing crons pause NAME | sparkwing schedules pause --schedule NAME |
sparkwing crons resume NAME | sparkwing schedules resume --schedule NAME |
sparkwing crons reset NAME | sparkwing schedules set --schedule NAME --reset |
- Exit codes and output: each replacement prints what the old verb printed,
in every
-oformat, and exits the same way.crons list --timerstill exits 1 when schedules are armed and the timer is not evaluating them, so a check script only changes its command line.-o jsonkeeps the old shapes: the health record for--timer, one NDJSON instant per line for--next, the schedule row for--pin,--unpin,--pauseand--resume, and{"schedule": ..., "name": ...}foruninstall --name. Error messages name the new spelling, such ascrons set --pause:. - Defaults:
crons nextshowed 5 instants unless--countsaid otherwise;--nexttakes the count as its value, so write--next 5for the old default. - Profiles:
--profile NAMEworks on each replacement as it did on the old verb.--pinand--unpinrefuse it, aslockandunlockdid. - One action per call:
--pin,--unpin,--pause,--resumeand--reseteach stand alone oncrons set, and none mixes with--cron,--tz,--overlap,--catch-upor--arg.crons listtakes one of--all,--timerand--next.uninstall --namerefuses--fleet, and leaves the OS timer in place asdisarmdid. - Positionals:
crons listandcrons uninstallnow refuse a stray positional.crons uninstall NAMEused to ignore the name and disarm the whole repository. - Timer units: systemd and launchd units run
sparkwing __tick. Refresh installed units withsparkwing schedules install.
Docs verbs fold into list, read and migrations
| Before | After |
|---|---|
sparkwing docs guides | sparkwing docs list --guides |
sparkwing docs versions [--web] [--no-cache] | sparkwing docs list --versions [--web] [--no-cache] |
sparkwing docs all | sparkwing docs read --guide authoring |
sparkwing docs migrations list [--web] | sparkwing docs migrations [--web] |
sparkwing docs migrations read --version V | sparkwing docs migrations --version V |
sparkwing docs migrations read V | sparkwing docs migrations --version V |
sparkwing docs migrations between --from A --to B | sparkwing docs migrations --from A --to B |
sparkwing docs migrations between | sparkwing docs migrations --from v0.0.0 |
sparkwing docs cache info | sparkwing cache info --docs |
sparkwing docs cache clear | sparkwing cache prune --docs |
- Agents: a prompt, skill or script that runs
docs migrations read --version Vordocs migrations between --from A --to Bdrops thereadorbetweenword and keeps its flags. A baredocs migrationsnow lists the guides; the every-guide blob that barebetweenprinted needs--from v0.0.0. The old words fail loudly:docs migrations read --version Vis refused as an unexpected positional, and a baredocs migrations readas an invalid version. - Output and exit codes: each replacement prints what the old verb printed
in every
-oformat and exits the same way, with two exceptions.cache info --docsheads its pretty reportDOCS WEB CACHEinstead ofCACHE, and with-o plainprints the cache directory wheredocs cache inforefused plain.cache prune --docshonours-o:-o jsonprints{"dir": ..., "removed": N}and-o plainthe removed count, wheredocs cache clearprinted its sentence for every format. - Flag combinations:
--guidestakes only--output;--versionstakes--weband--no-cache;read --alltakes only--output.--versionand--from/--toondocs migrationscannot be combined.--docsoncache infoandcache prunerefuses--all,--max-bytesand--max-entries.docs listnow refuses a stray positional.
Connection verbs fold into cloud
| Before | After |
|---|---|
sparkwing configure profiles add --name N --controller URL --token-stdin | sparkwing profiles add --profile N --controller URL --token-stdin |
sparkwing configure profiles add --name N --controller URL (controller not up, or unauthenticated) | sparkwing profiles add --profile N --controller URL --no-probe |
sparkwing configure profiles add ... --token T | printf %s "$T" | sparkwing profiles add --profile N --controller URL --token-stdin |
sparkwing configure profiles remove --name N | sparkwing profiles remove --profile N --keep-token |
sparkwing configure profiles test --profile P [-o json] | sparkwing profiles show --profile P [-o json] |
sparkwing cluster status --profile P [-o json] | sparkwing runners list --profile P [-o json] |
sparkwing profile [--profile P] [-o json] | sparkwing profiles show --profile P [-o json] |
sparkwing cluster tokens lookup --prefix X --profile P | sparkwing tokens list --token X --profile P |
sparkwing configure profiles duplicate --src A --dst B | copy the A: entry under profiles: in config.yaml to B: |
profiles addprobes by default: it checks that the controller answers and that the piped token authenticates unless--no-probeis given, and refuses an existing profile name unless--forceis given.--tokenon the command line is gone; pipe the token to--token-stdin.profiles show --profile Pprints one saved profile with its token prefix and does not contact the controller. Check authenticated controller access withrunners list --profile P.
Repository verbs gather under repos
| Before | After |
|---|---|
sparkwing configure xrepo add [path] | sparkwing --checkout PATH repos add |
sparkwing configure xrepo remove <path-or-basename> | sparkwing --checkout PATH repos remove |
sparkwing configure xrepo prune | sparkwing repos prune |
sparkwing configure xrepo list [--pipelines=false] [-o json] | sparkwing repos list --checkouts [--pipelines=false] [-o json] |
sparkwing repos [-o json] | sparkwing repos list [-o json] |
sparkwing update --sdk [--version V] | sparkwing repos update --in-place [--version V] |
sparkwing update --sdk --check | sparkwing repos update --in-place --dry-run |
repos update --in-placeruns the oldupdate --sdkpath unchanged: nativego getfor the resolved release, thengo mod tidy, in the checkout you stand in or the onesparkwing -C DIRnames. It neither compares plans nor commits, and it refuses--apply,--verifyand--repo, which act on the tracked fleet.--in-place --checkkeeps the exit codes: 0 current or ahead, 1 an update is available, 2 unknown, diverged or a failed check.sparkwing updateupdates the CLI only.--clistill names that target;--forceand--override-holdare unchanged.
daemon explain moves to daemon events --explain
| Before | After |
|---|---|
sparkwing daemon explain --run ID [-o json] | sparkwing daemon events --run ID --explain [-o json] |
The output and exit codes are unchanged.
cluster image rollout is removed
Before: sparkwing cluster image rollout --image NAME --tag TAG [--wait] [--tail-logs].
The removed command edited images[].newTag in a gitops checkout's kustomization.yaml, committed
and pushed, ran argocd app sync, and waited with kubectl rollout status.
Run those steps from a pipeline job instead:
cd "$GITOPS_REPO" && kustomize edit set image "NAME=REGISTRY/NAME:TAG"
git commit -am "rollout NAME TAG" && git push
argocd app sync APP && kubectl rollout status deploy/NAME -n NAMESPACE
SPARKWING_GITOPS_REPO is no longer read.
Pipeline steps no longer see SPARKWING_AGENT_TOKEN
- Before: the pipeline binary kept the runner's
SPARKWING_AGENT_TOKENin its environment, so every command a step started inherited it, and a step that printed it showed the raw value in the run's logs..CacheDirused it as the cache bearer when noSPARKWING_CACHE_GRANTorSPARKWING_CACHE_TOKENwas set. - After: the pipeline binary removes the token from its environment when
it starts and masks it in node output. Commands a step starts do not see it,
and
.CacheDirsends only the cache grant or cache token. - Author steps: a step that called the controller with
$SPARKWING_AGENT_TOKENuses the SDK instead (sparkwing.Secret,sparkwing.RunAndAwait), which reaches the controller through the node's own connection. - Operator steps: a cache that accepted the agent token because its API
token was set to the same value now needs the controller to mint cache grants
(
SPARKWING_CACHE_GRANT_KEYon the controller and the cache), or the dependency cache runs without the service.
cmd/sign-manifest is removed
- Before:
go run ./cmd/sign-manifest -genkeyprinted an Ed25519 keypair, and the same helper could sign and verify a manifest. - After: the helper is gone. The release workflow signs and verifies
with
cmd/verify-release. - Operator steps: generate a seed with
openssl rand -base64 32, store it asSPARKWING_UPDATE_SIGNING_KEY, and print its public key withSPARKWING_RELEASE_SIGNING_KEY=<seed> go run ./cmd/verify-release --public-key.
Storage and store names that moved
- Before:
storeurl.OpenS3,storeurl.SDKMaxAttemptsandstoreurl.SDKMaxBackofflived ininternal/storage/storeurl. - After: they are
Open,SDKMaxAttemptsandSDKMaxBackoffininternal/storage/s3, besideParseURLandNewClient. Behaviour is unchanged. - Author steps: replace
storeurl.OpenS3(ctx, raw)withs3.Open(ctx, raw)fromgithub.com/sparkwing-dev/sparkwing/internal/storage/s3.
The controller's dispatcher hook is removed
- Before:
controller.Server.WithDispatchertook acontroller.Dispatcherthat was called with acontroller.RunRequestafter each trigger was recorded, defaulting tocontroller.NoopDispatcher. - After: the types and the option are gone. A trigger is recorded and a runner claims it by polling, as with the default before.
- Author steps: remove the
WithDispatchercall. Start work for a recorded trigger by claiming it (POST /api/v1/triggers/claim, orclient.ClaimTriggerFor).
Removed store and release helpers
- Before:
store.Store.ChargeNodeCreditsbilled a node's elapsed seconds directly, andgo run ./cmd/sign-manifest -genkeyprinted a signing key. - After: both are gone; node renewal, finish and launch settlement bill inside their own transactions.
- Author steps: settle a node with
FinalizeNodeCredits; generate a release seed as described above undercmd/sign-manifest is removed.
JobSpawn and JobSpawnEach are removed
Before: a step graph inserted Plan nodes while it ran. JobSpawn
added one child named parent/id, and JobSpawnEach one per item; the
parent's runner stayed suspended until every child finished.
func (j *Scan) Work(w *sw.Work) (*sw.WorkStep, error) {
list := sw.Step(w, "list", j.listTargets)
sw.JobSpawnEach(w, j.targets, func(t string) (string, any) {
return "scan-" + t, &ScanTarget{Target: t}
}).Needs(list)
return nil, nil
}
After: Jobs are added on the Plan. A Job produces the item list, and
JobFanOutDynamic creates one child per item once it succeeds; the
source's runner exits first.
type ListTargets struct {
sw.Base
sw.Produces[[]string]
}
func (j *ListTargets) Work(w *sw.Work) (*sw.WorkStep, error) {
return sw.Step(w, "list", j.list), nil // func(ctx) ([]string, error)
}
func (Scan) Plan(ctx context.Context, plan *sw.Plan, _ sw.NoInputs, rc sw.RunContext) error {
list := sw.Job(plan, "list", &ListTargets{})
sw.JobFanOutDynamic(plan, "scan", list, func(t string) (string, any) {
return "scan-" + t, &ScanTarget{Target: t}
})
return nil
}
Author steps:
- A single
JobSpawn(w, id, job)becomessw.Job(plan, id, job)with.Needs(...)on the Job that ran the steps before it. Steps after the spawn move into a Job thatNeedsthe new one, and read its output withsw.RefTo[T](node). - A
JobSpawnEachbecomes the Plan shape above. SkipIfon a spawn becomesSkipIfon the new Job.- Child node ids lose the
parent/prefix; update anything that reads runs by node id, such as--sw-onlyglobs and dashboards.
Tools that read plan snapshots, pipeline plan --json or the run API's
work object stop seeing spawns, spawn_each, cardinality and
cardinality_source, and the spawn_dispatched event is no longer
written.
The job-args schema builder is removed
Before: a job embedding sparkwing.WithArgs[T] turned every exported
field of T into a kebab-cased flag, and declared constraints in Go
through a Schema() method built with sparkwing.NewSchema[T]().
sparkwing.Arg[T] and ArgOrDefault read any job's arg by flag name.
type DeployArgs struct {
Replicas int
Image string
Strategy string
}
func (DeployJob) Schema() (*sparkwing.Schema, error) {
s := sparkwing.NewSchema[DeployArgs]()
s.Field("Replicas").Default(3)
s.Field("Image").Required()
s.Field("Strategy").Default("rolling").OneOf("rolling", "recreate")
return s.Build()
}
After: T uses the pipeline Inputs tags, and only flag:-tagged
fields are flags. Required, default and enum checks run before any step,
as they do for Inputs.
type DeployArgs struct {
Replicas int `flag:"replicas" default:"3"`
Image string `flag:"image" required:"true"`
Strategy string `flag:"strategy" default:"rolling" enum:"rolling,recreate"`
}
Author steps:
- Add a
flag:"name"tag to everyTfield that should stay a flag, using the kebab-cased name the field had (MainSourceRootwas--main-source-root). An untagged field now fails as an unknown flag when passed. - Move constraints into tags and delete the
Schema()method:
| Removed | Use instead |
|---|---|
.Required() | required:"true" |
.Default(v) | default:"v" |
.OneOf(a, b) | enum:"a,b" (string fields; needs default or required) |
.Min, .Max, .Range, .Positive, .Custom | check the value at the top of the step and return an error |
.Computed(fn), .DependsOn | compute the value inside the step from j.Args(ctx) |
.RequiredWhen(p), Group(...), predicates | validate the combination inside the step and return an error |
.Bind("target") | a flag:"target" tag; Bind only checked its argument |
sparkwing.Arg[T](ctx, flag), ArgOrDefault | j.Args(ctx) on the job that declares the flag, or a pipeline Inputs field read in Plan |
Move a secret:"true" arg to the pipeline's Inputs: the run masks only
pipeline secrets, so a job secret panics at registration.
Tools that read Plan.TransitiveArgsSurface or JobArgSchemas use
Plan.JobArgs, which returns DescribeArg records stamped with the
owning job id.
Six unused SDK names are removed
Before: the sparkwing package exported Cache, Logs and State (type
aliases for the internal/storage interfaces), TypeName, FailureFromContext and
(*SpawnSpec).ResolvedID.
After: none of them exist. No pipeline, sparks library or guide used them.
Author steps:
| Removed | Use instead |
|---|---|
sparkwing.Cache, sparkwing.Logs, sparkwing.State | storage.ArtifactStore, storage.LogStore, storage.StateStore from github.com/sparkwing-dev/sparkwing/internal/storage |
sparkwing.TypeName(p) | reflect.TypeOf(p).Elem().Name() for a pointer, or reflect.TypeOf(p).Name() |
sparkwing.FailureFromContext(ctx) | the sparkwing.Failure argument an OnFailure handler already receives |
(*SpawnSpec).ResolvedID() | nothing; it always returned "" |
The sparkwing-web service is gone: the controller serves the dashboard,
sign-in, and the browser flows on its own listener. Operators repoint the
console host at the controller and delete the web Deployment; sparkwing dashboard start users change nothing.
The controller serves the dashboard
Before: sparkwing-web hosted the dashboard bundle, the login page, OAuth
and GitHub App browser flows, and a proxy that forwarded an allowlist of
/api/v1/* routes to the controller and log reads to the logs service. The
sparkwing-full chart deployed it as <release>-web, and its Ingress routed
there.
After: sparkwing-controller serves the dashboard pages, /login,
password and first-admin sign-in, logout, /auth/{provider}/start and
/callback, identity linking at /auth/{provider}/link, the GitHub App pages
under /github/app/, and the dashboard's log, grep, event-stream and capacity
reads, on the same port as its API. Session cookies are __Host-sw_session and
__Host-sw_csrf. Log reads go to the logs service named by the controller's
--logs-url, carrying the caller's own credential. Pages require sign-in
whenever the controller enforces token auth. A controller built without
bin/build-web.sh still starts, and its pages answer 503 naming that step.
The chart deploys no web Deployment or Service, and its Ingress routes every
host to the controller Service.
Upgrade:
-
Point the console host's Ingress or load balancer at the controller Service. A console host and an API host can both point at it; a reverse proxy in front can still split them.
-
Delete the
sparkwing-webDeployment and Service. Asparkwing-fullupgrade removes them and names the Ingress after the controller. A values file that still setswebfails to render: moveweb.logs.urltocontroller.logs.urland drop the rest. -
Move
sparkwing-webflags to the controller:sparkwing-webController --require-loginFollows the controller's own auth: pages require sign-in whenever it enforces tokens --hsts--hsts--trusted-proxy-addr--trusted-proxy-addr, which also readsX-Forwarded-Protoas TLS evidenceSPARKWING_WEB_INSECURE_COOKIESwith--allow-insecure-cookies-remote--insecure-cookies--controller,--logs,--token,--profile,--state-spec,--logs-spec,--artifacts-specNone; the controller reads its own state and uses --logs-url--allow-unauthenticated-remote,--allow-originNone; the controller refuses cross-site browser writes to /api/Chart users:
ingress.tlspasses--hsts, andingress.allowInsecure=truewithout TLS passes--insecure-cookies. -
A dashboard published over plain HTTP needs
--insecure-cookies(chart:ingress.allowInsecure=true), or browsers drop theSecuresession cookies. -
Register the OAuth and GitHub App callback URLs on the host that now reaches the controller, and list the sign-in callbacks in
--oauth-redirect-uris. The paths are unchanged.
sparkwing dashboard start runs the controller in local mode with the dashboard
attached and needs nothing.
Removed routes: these answer 404. No CLI command called them; a client of your own drives the browser pages that replace them.
| Method | Path |
|---|---|
POST | /api/v1/auth/oauth/google/start |
POST | /api/v1/auth/oauth/google/exchange |
POST | /api/v1/auth/oauth/github/start |
POST | /api/v1/auth/oauth/github/exchange |
POST | /api/v1/me/identities/{provider}/link |
POST | /api/v1/me/identities/{provider}/link/complete |
POST | /api/v1/team/github-app/connect |
POST | /api/v1/team/github-app/connect/available |
POST | /api/v1/team/github-app/connect/select |
POST | /api/v1/team/github-app/connect/complete |
GET | /api/v1/operator/session |
GET | /api/v1/auth/bootstrap-needed |
/api/v1/auth/login, /api/v1/auth/session and /api/v1/auth/logout remain.
Why: two processes split one browser surface, and the proxy's route allowlist and service bearer were a second authorization layer to keep in step with the controller's own.
Node bounce requests move to their run's team
- Before: a bounce request was recorded without its team, so every row carried the default team whatever team its run belonged to.
- After: schema 93 moves each existing request into the team of the run it names, and new requests record their run's team. A team's open requests stay visible to its runners, and its next request continues the node's sequence.
- Upgrade: nothing to do; the controller applies schema 93 on start. An older binary keeps reading the upgraded database.
Store methods removed or moved to Tenant
- Before:
internal/storeexportedStore.ActiveExecutorActivity,Store.PrincipalHoldsPipelineClaimandStore.FailNodeForUnpricedClass. - After: the first two are gone, and the third is a method of
Tenant. Many otherStoremethods now act on the default team only; theirTenantmethods of the same name serve every team. - Upgrade: code embedding
internal/storereplacesst.ActiveExecutorActivity(ctx, now)withteam.ActiveExecutorActivity(ctx, now)on the handle fromst.ForTeam(ctx, slug), replacesst.PrincipalHoldsPipelineClaimwithst.PrincipalHoldsProfileClaimorst.PrincipalHoldsRunClaim, and callsFailNodeForUnpricedClasson the run's team handle. Code that reads or writes runs, nodes or triggers of a team other than the default goes through that team's handle.