Error reference
Every validation failure carries a stable [E_*] code. Match on the code; the
message always names the offending resource and the fix. All of these fail
fluffy-chainsaw scan (and everything built on it) — you fix them in code or
fluffy-chainsaw.yaml, never by retrying.
Code ↔ yaml correspondence
| Code | Meaning | Fix |
|---|---|---|
E_NOT_IN_CODE |
fluffy-chainsaw.yaml configures a resource under databases/buckets/topics/secrets/runners that no code marker declares |
Remove the yaml entry, or add the marker (db.connect, storage.bucket, pubsub.topic, secret.get, cloudrun.entrypoint) in code |
E_NAME_COLLISION |
The same name is used in two collections (e.g. a database and a bucket both named users) |
Rename one — names are unique across all collections |
Databases & instances
| Code | Meaning | Fix |
|---|---|---|
E_DB_UNBOUND |
A database names no instance — placement is never inferred | Set databases.<name>.instance, or a defaults.databases.instance |
E_INSTANCE_UNDECLARED |
A database binds to an instance nothing declares | Add the instance under instances: (or fix the name) |
E_INSTANCE_ORPHANED |
A declared instance no database binds to (an idle, billed server) | Bind a database to it or remove it |
E_USES_UNKNOWN |
A runner's uses.databases lists a database code never declares |
Add db.connect("<name>") in code, or remove it from uses |
E_DATABASE_NAME_INVALID |
Database name breaks Postgres rules | 1–63 chars, start with a letter, then letters/digits/underscores |
E_INSTANCE_NAME_INVALID |
Instance name breaks Cloud SQL rules | 1–98 chars, start with a lowercase letter, then lowercase letters/digits/dashes |
Buckets
| Code | Meaning | Fix |
|---|---|---|
E_USES_BUCKET_UNKNOWN |
A runner's uses.buckets names a bucket code never declares |
Add storage.bucket("<name>") in code, or remove the binding |
E_BUCKET_NO_PREFIX |
A bucket binding has no prefix — runners cannot access bucket root |
Set prefix: <something>/ on the binding |
E_BUCKET_INVALID_PREFIX |
The prefix doesn't end with /, or contains .. / \ |
Use a clean trailing-slash prefix like uploads/ |
E_BUCKET_NO_ACCESS |
A bucket binding grants nothing | Set viewer: true and/or writer: true |
E_BUCKET_DUPLICATE |
A runner binds the same bucket twice | One binding per bucket per runner |
E_BUCKET_NAME_INVALID |
Logical bucket name breaks the naming rule | 3–45 chars of lowercase letters, digits, dashes, underscores |
E_BUCKET_LIFECYCLE |
abort_incomplete_upload_days ≤ 0 |
Use a positive number of days |
E_BUCKET_EXISTING_CONFIG |
Owner-only knob (public/versioning/lifecycle/location) set on an existing: true bucket |
Remove the knobs — you can't configure a bucket another app owns |
Topics & push subscriptions
| Code | Meaning | Fix |
|---|---|---|
E_USES_TOPIC_UNKNOWN |
A runner publishes to a topic code never declares | Add pubsub.topic("<name>") in code, or remove it from uses.publishes |
E_SUB_TOPIC_UNKNOWN |
A subscription binds to an undeclared topic | Add pubsub.topic("<name>") (or existing: true for a topic another app owns) |
E_SUB_DLQ_UNKNOWN |
dead_letter names an undeclared topic |
Declare the DLQ topic — it's just a normal declared topic |
E_SUB_NOT_IN_CODE |
A yaml subscription path has no pubsub.subscription(path, handler) in code |
Add the handler for that exact path |
E_SUB_NOT_IN_YAML |
Code declares pubsub.subscription(path, handler) but no runner binds that path |
Add trigger: { subscriptions: [{ topic: ..., path: <path> }] } to a runner |
E_SUB_PUBLIC_RUNNER |
A runner has both trigger.http: true and push subscriptions |
Move the subscriptions to their own runner without trigger.http |
E_SUB_PATH_SHAPE |
Subscription path doesn't start with / |
Use an absolute path like /on-order |
E_SUB_ACK_DEADLINE |
ack_deadline outside GCP's bounds |
Use 10–600 seconds |
E_SUB_MAX_RETRIES |
max_retries outside GCP's bounds |
Use 5–100 (and only together with dead_letter) |
E_TOPIC_NAME_INVALID |
Topic name breaks GCP's rule | 3–255 chars, start with a letter, then letters/digits/- . _ ~ + %; must not start with goog |
Schedules (cron)
| Code | Meaning | Fix |
|---|---|---|
E_SCHEDULE_NOT_IN_CODE |
A yaml schedule path has no cloudrun.scheduled(path, handler) in code |
Add the handler for that exact path |
E_SCHEDULE_NOT_IN_YAML |
Code declares cloudrun.scheduled(path, handler) but no runner schedules that path |
Add trigger: { schedules: [{ path: <path>, cron: "..." }] } to a runner |
E_SCHEDULE_PATH_SHAPE |
Schedule path doesn't start with / |
Use an absolute path like /jobs/daily |
E_SCHEDULE_NO_CRON |
A schedule entry has no cron expression | Add cron: "0 6 * * *"-style expression |
E_TRIGGER_PATH_DUP |
One runner reuses a path across schedules/subscriptions (they share one router) | Make every schedule/subscription path unique within the runner |
Secrets
| Code | Meaning | Fix |
|---|---|---|
E_USES_SECRET_UNKNOWN |
Either: a runner's uses.secrets names a secret code never declares, or a code-declared secret is granted to no runner |
Match secret.get("<name>") in code with uses: { secrets: [<name>] } on a runner; or opt out with defaults: { secrets: { unused: true } } |
E_SECRETS_NO_PROJECT |
A runner uses secrets but has no GCP_PROJECT env |
Add env: { GCP_PROJECT: <project-id> } to the runner |
Auth
| Code | Meaning | Fix |
|---|---|---|
E_USES_AUTH_NO_SINGLETON |
A runner sets uses.auth: true but there is no top-level auth: block |
Add auth: { engine: firebase, providers: [...], session: { ttl: 7d } } |
E_AUTH_PROVIDER_UNKNOWN |
auth.providers lists an unsupported provider |
Supported today: google, github |
E_AUTH_TTL_RANGE |
session.ttl unparseable or outside Firebase's allowed range |
Use a duration like 7d, 12h, 30m, within [5m, 14d] |
E_ROUTER_INVALID |
auth.router names a runner that doesn't exist, isn't HTTP-triggered, or doesn't set uses.auth: true |
The named runner must be HTTP-triggered (trigger: { http: true }) and opt into auth |
Realtime
| Code | Meaning | Fix |
|---|---|---|
E_USES_REALTIME_NO_SINGLETON |
Either: a runner sets uses.realtime: true with no top-level realtime: block, or the block is declared and used in code but no runner opts in |
Add realtime: { engine: firebase } and uses: { realtime: true } on the publishing runner |
E_REALTIME_NOT_IN_YAML |
Code calls realtime.channel() but there is no realtime: block |
Add realtime: { engine: firebase } and uses.realtime: true on the publishing runner |
E_REALTIME_NOT_IN_CODE |
A realtime: block is declared but code never calls realtime.channel() |
Remove the block, or publish to a channel in a runner |
E_ROUTER_INVALID |
realtime.router names a runner that doesn't exist, isn't HTTP-triggered, or doesn't set uses.realtime: true |
The named runner must be HTTP-triggered (trigger: { http: true }) and opt into realtime |
E_REALTIME_INSTANCE_INVALID |
realtime.instance is not a valid RTDB instance id |
1–30 chars: lowercase letters, digits, dashes; no leading/trailing dash. Instance ids are globally unique |
Hosting & workspace
| Code | Meaning | Fix |
|---|---|---|
E_HOSTING_SITE_INVALID |
hosting: declared without a site, or the site id is invalid |
Set hosting.site (1–30 chars: lowercase letters, digits, dashes; globally unique — it becomes <site>.web.app), or omit the block for the project's default site |
E_WORKSPACE_DOUBLE_OWNERSHIP |
Two children of the workspace manifest both own the same physical resource (neither marks it existing: true) |
Exactly one child owns a shared resource; mark it existing: true in every other child, or give each child its own name (e.g. its own hosting.site / realtime.instance) |
Stacks & releases
| Code | Meaning | Fix |
|---|---|---|
E_STACK_PROJECT_COLLISION |
Two stacks in deploy.project bind the same GCP project |
One project per stack — resource names are identical across stacks and would collide in a shared project |
Stack failures without a code (schema/pipeline errors — the message names the exact path):
| Message contains | Meaning | Fix |
|---|---|---|
no value for stack "X" |
A {stack: value} map doesn't cover every declared stack |
Add the missing stack's value — there is no fallback |
unknown stack "X" |
A per-stack map names a stack not in stacks: |
Fix the typo or declare the stack |
requires a top-level stacks: list |
A {stack: value} map with no stacks: declared |
Add stacks: [...], or flatten the map to a scalar |
cannot vary per stack |
A per-stack map on a field that must stay scalar (deploy.stack, deploy.select, deploy.infra-root, instances.region) |
Use a plain scalar |
per-stack maps do not nest |
A map inside a per-stack map's value | Per-stack values are scalars |
stack "X" is not declared |
--stack (or a workspace entry's stack:) names a stack outside stacks: |
Declare it or fix the flag |
differ beyond instance sizing |
Something other than instance sizing varies between stacks | Only instances.{tier, disk_size, availability_type, database_version, disk_autoresize, deletion_protection} (and CLI-side deploy.*) may differ; make everything else identical |
is pinned … but releases/<stack>.images.json is missing |
deploy.pinned stack with no committed release manifest |
Run fluffy-chainsaw release --to <stack> and commit the manifest |
not digest-pinned |
A release manifest ref lacks @sha256:… |
Re-run fluffy-chainsaw release — hand-edited tags are refused |
Migrations
Config-time (from fluffy-chainsaw scan and everything built on it):
| Code | Meaning | Fix |
|---|---|---|
E_MIGRATIONS_NO_RUN |
A database declares migrations without a run command |
Set databases.<name>.migrations.run to your migration tool command |
E_MIGRATIONS_EXISTING |
migrations declared on an existing: true database |
Remove it — a referenced database's schema belongs to the owning app |
Run-time (from fluffy-chainsaw migrate and the migrate stages of local/deploy):
| Code | Meaning | Fix |
|---|---|---|
E_MIGRATE_NOTHING |
fluffy-chainsaw migrate ran but no database declares migrations |
Add databases.<name>.migrations.run |
E_MIGRATE_LOCAL_DOWN |
--local but the compose postgres isn't running |
Run fluffy-chainsaw local (or --deps-only) first |
E_MIGRATE_CONNECT |
Can't connect as the migrator | Locally: the volume predates the migrations block — fluffy-chainsaw local-prune and restart. Deployed: provision hasn't run since migrations were declared |
E_MIGRATE_NO_OUTPUT |
The stack exports no migrations entry for the database |
Run fluffy-chainsaw provision (or deploy) after declaring migrations |
E_MIGRATE_PROXY_MISSING |
cloud-sql-proxy is not installed |
brew install cloud-sql-proxy |
E_MIGRATE_PROXY |
The proxy didn't become ready | Check your gcloud identity may impersonate dbmig-<db>@… (it must match the stack's deployerPrincipal) |
E_MIGRATE_TOOL_FAILED |
Your run command exited non-zero |
Read the tool's output above the error — the failure is the migration's |
E_MIGRATE_GRANT_SYNC |
Granting DML to the runner identities failed | Usually tables the migrator doesn't own (pre-migrations boot-DDL) — transfer ownership once; see migrations.md |
Failures without an E_* code
- Schema errors — unknown collection, unknown field, wrong value type, or an
out-of-set enum value in
fluffy-chainsaw.yaml. The message points at the exact path (e.g.runners.api.trigger.ingress: "intranet" is not one of [all internal internal-load-balancing]). Fix the yaml; see fluffy-chainsaw-yaml.md for the full field set. - Dynamic marker names —
db.connect(tenantId)or any name that isn't a string literal / file-localconststring fails the scan with the file and line. Make the name static. - Marker with too few arguments — e.g.
cloudrun.scheduled()without a path. The scan names the call site; pass the required name/path.