Runners

A runner is one deployable service (Cloud Run) with its own identity. Code declares that it exists; fluffy-chainsaw.yaml decides what it may touch, how it is reached, and how it builds.

Prerequisites

The markers

cloudrun.entrypoint(name, handler?)

import { cloudrun } from "@org/fluffy-chainsaw";

cloudrun.entrypoint("orders-api", (req, res) => {
  res.end("hello");
});

cloudrun.scheduled(path, handler) — cron routes

cloudrun.scheduled("/jobs/daily-digest", async (_req, res) => {
  await sendDigests();
  res.end("ok");
});

Pair each scheduled path with a cron expression in yaml:

runners:
  orders-api:
    trigger:
      schedules:
        - { path: /jobs/daily-digest, cron: "0 6 * * *" }

The pairing is two-way checked: a yaml schedule with no cloudrun.scheduled handler (or vice versa) fails fluffy-chainsaw scan. Schedule and subscription paths on one runner share a router, so they must be unique (E_TRIGGER_PATH_DUP).

fluffy-chainsaw.yaml

runners:
  orders-api:
    uses:                          # least privilege — a map per binding kind, NOT a flat list
      databases: [users, orders]
      buckets:
        - { name: media, prefix: uploads/, writer: true }
      publishes: [orders]          # topics it may publish to
      secrets: [stripe-key]
      auth: true                   # opt into the auth plugin
    kind: cloud-run                # optional; only "cloud-run" today
    trigger:
      http: true                   # public; omit/false = private (IAM callers only)
      ingress: all                 # all | internal | internal-load-balancing
      schedules: []                # see above
      subscriptions: []            # see pubsub.md
    build:
      target: orders-api           # make target (default: runner name)
      makefile: Makefile           # optional
    env:                           # string -> string, set on the service
      LOG_LEVEL: info
    vpc:                           # optional egress via a Serverless VPC connector
      connector: my-connector
      egress: private-ranges-only  # all-traffic | private-ranges-only

Networking: two independent axes

Goal yaml
Public (internet, unauthenticated) trigger: { http: true }
Private (IAM-authenticated callers only) omit trigger.http (or false)
Internal only (VPC / same project) trigger: { ingress: internal }
Internal via load balancer trigger: { ingress: internal-load-balancing }
Egress to private-IP resources vpc: { connector: <name>, egress: private-ranges-only }

The axes compose — e.g. a locked-down internal service:

trigger: { http: false, ingress: internal }
vpc: { connector: my-connector, egress: all-traffic }

Calling another runner (local)

import { runner } from "@org/fluffy-chainsaw";

const res = await fetch(`${runner.url("billing")}/charge`, { method: "POST" });

Under fluffy-chainsaw local, every runner gets RUNNER_URL_<NAME> pointing at its sibling containers; runner.url(name) resolves it (and throws with a fix-it message if the runner isn't declared or running).

Gotchas