BuildBakeryRequest access

Writing recipes

A recipe tells BuildBakery how to bake your app: one workflow per thing you want built (iOS, Android, a Linux binary…). Each workflow becomes a job on an oven that can run it, and they all bake in parallel.

Where the recipe lives

Put a codebuilder.yaml file at the root of your repository (or of the folder you upload). BuildBakery reads it at the start of every batch, so the recipe is versioned with your code. You can also:

  • Override it per project on the Recipes tab (or bakery recipe push FILE). The project recipe then wins over the file in the repo.
  • Override it for one batch with "Use a different recipe for this batch" in the Bake dialog, or bakery bake --recipe FILE.

Check a recipe any time with bakery recipe validate or the Validate button on the Recipes tab.

Your first recipe

workflows:
  web:
    platform: web
    steps:
      - npm ci
      - npm run build
    artifacts:
      - dist/**

That's a complete recipe: one workflow called web that installs dependencies, builds, and keeps everything in dist/. Each step is a bash script run from the root of your source.

Workflows

Everything under workflows: is a workflow, keyed by a short name (letters, numbers, -, _, .). You can bake just some of them: bakery bake --workflows ios,android.

FieldWhat it does
nameDisplay name (defaults to the key).
platformios, android, macos, linux, windows, web or any. Decides which ovens can bake it (below).
runs_onOverride the oven OS: macos, linux, windows, or a list like [linux, macos].
labelsTools the oven must have, e.g. [xcode] or [android-sdk]. Ovens advertise what they have preinstalled.
timeout_minutesStop the job after this long (default 60, max 1440).
envPlain environment variables for every step. Use Ingredients for secrets.
stepsWhat to run, in order (required).
artifactsFiles to keep from the build (glob patterns).
deliverdeliver: { folder: android } puts this workflow’s artifacts in a subfolder of every SFTP/bucket delivery.
imageLinux ovens: run in this Docker image instead of the BuildBakery default.
shellbash (default), pwsh, powershell or cmd, for every step; a step can set its own. powershell and cmd need runs_on: windows.
manualtrue: only runs when a batch names it (bakery bake --workflows deploy), e.g. a deploy; normal batches and pushes skip it.
toolsTool versions to install before the steps, e.g. node: 20, python: "3.12", dotnet: 9 (any tool mise supports).
cacheExtra cache paths, or false to turn caching off.
code_signingiOS/macOS automatic code signing.
android_signingSign Android builds with a keystore stored on the project.
integrationsGive steps App Store Connect / Microsoft Store credentials for publishing.
publishpublish: { app_store_connect: true } uploads the IPA to App Store Connect / TestFlight at the end.
verify_signingSigning guard: fail the job unless its APK/AAB/IPA is signed with the expected certificate.
oven_poolown (only your private ovens), hosted (only BuildBakery ovens) or any (default: yours first).

Platforms and ovens

platformBakes onPreinstalled
ios, macosMac ovens (Apple silicon)Xcode, CocoaPods, fastlane, Flutter, Node 22, JDK 17, xcbeautify, SwiftLint, XcodeGen
androidLinux ovensAndroid SDK 34/35, build-tools, NDK 27, CMake, JDK 17, Flutter, Node 22
linux, webLinux ovens, including ones on Windows PCs (WSL or Docker Desktop); web also on Macsgcc/clang tooling, Node 22 + yarn + pnpm, Bun, Python 3, Ruby, Go, Rust, JDK 17 + 21, .NET 8, PHP + Composer, CMake. Any other tool or version: tools:
windowsWindows ovens (builds run directly on Windows)Whatever the PC has: Visual Studio / Build Tools, MSBuild, .NET, Git Bash, PowerShell

Bring your own machine as an oven from the Ovens page or bakery ovens add: a Linux box, an Apple-silicon Mac, or a Windows PC (it builds Linux jobs in WSL or Docker Desktop, or Windows jobs natively). The oven picks how many jobs it runs at once from its CPUs and RAM (about 2 cores and 6 GB per Android build); Edit on the Ovens page shows the suggestion. If no oven can run a job (say, iOS with no Mac oven), the batch says so and you can keep it queued until one is added, or cancel just that job.

Steps

A step is either a single command, or a named script:

steps:
  - npm ci                       # short form: the name is the command
  - name: Build release
    script: |
      ./gradlew assembleRelease \
        -PversionCode=$CB_BUILD_NUMBER
  - name: Lint (don't fail the batch)
    script: npm run lint
    ignore_failure: true
  - name: Build the iOS part
    working_directory: ios
    script: pod install
  • Scripts run in bash with set -eo pipefail: the first failing command fails the step, and a failed step stops the job (unless ignore_failure: true).
  • Every step starts in the source root (or working_directory), with a fresh shell; files persist between steps, shell variables don't.
  • YAML tip: quote a step that contains : (e.g. - 'echo "error: x"') or use the | block form.
  • On Windows ovens a step (or the whole workflow) can use another shell: shell: pwsh, powershell or cmd. Containers and Mac VMs always use bash.

Tools and versions

Every oven comes with the common tools (see the table above). Need something else, or a specific version? List it under tools: and the oven installs it before the first step, using mise:

tools:
  node: 20            # 20, 20.11.1, lts or latest
  python: "3.11"      # quote versions like 3.10 (YAML reads 3.1)
  java: temurin-21
  dotnet: 9
  zig: 0.13.0
  • No tools:? The oven still installs what your repo pins: .nvmrc, .node-version, .python-version, .ruby-version, .java-version, .go-version, .tool-versions or mise.toml.
  • Installs are cached per project, so only the first build downloads them.
  • An unknown tool or version fails the job before the first step, with mise's message in the log.

Artifacts

artifacts:
  - android/app/build/outputs/**/*.aab
  - build/ios/ipa/*.ipa
  - build/macos/Build/Products/Release/*.app   # folders are zipped

Patterns are relative to the source root; ** matches any number of folders. Anything a step copies into $CB_ARTIFACTS_DIR is kept too. Artifacts are collected even when a batch burns, which helps debugging. Download them from the batch page or with bakery artifacts --download ./out.

Delivering artifacts

Artifacts always stay downloadable from BuildBakery. A project can also send them somewhere as soon as each job finishes (an APK doesn't wait for the rest of the batch): set it up in the project's Settings → Delivery (or bakery deliveries add …). Each delivery picks a destination, and optionally which workflows, branches and files it applies to. Test connection checks access without publishing anything.

Your server (SFTP)Copy files into a folder over SSH; optionally run a command there afterwards.
BucketAWS S3, Cloudflare R2, Backblaze B2, MinIO or any S3-compatible storage.
Google PlayUpload the .aab (or .apk) to a track: internal, closed, open or production; draft, full or staged rollout.
Firebase App DistributionSend the build to tester groups and emails, with release notes.
GitHub ReleaseCreate (or reuse) a release for a tag at the batch’s commit and attach the files. Drafts by default.
WebhookPOST signed JSON with 7-day download links to your own endpoint.

Accounts for destinations (servers, buckets, Google service accounts, webhooks) are added once on the Account page and can be used by all your projects. Secrets are encrypted and never shown again.

Folders, prefixes, tags and release notes can use placeholders:

{project}Project name
{batch}Batch number (12)
{workflow}Workflow key (android, ios…)
{platform}android, ios, macos, linux, windows, web
{branch}Git branch (upload for uploaded batches)
{commit}First 8 characters of the commit
{date}Batch date, 2026-11-03
  • By default only fresh (successful) jobs are delivered; you can include burnt ones. A job with no file matching the delivery's Files filter is skipped (a web job sends nothing to Google Play).
  • Failed deliveries show on the batch page with the reason and Retry, and post to the Slack channels that get “Burnt” notifications. Deliver again (or bakery deliver 12) re-sends a batch.

One folder per job

A delivery has one folder, but a batch can have several jobs. Give each workflow its own subfolder in the recipe with deliver.folder; it is added to the end of every SFTP folder and bucket prefix the job is delivered to:

# Delivery folder: ~/myproject
workflows:
  android:
    platform: android
    steps: [./gradlew assembleRelease]
    artifacts: [app/build/outputs/**/*.apk]
    deliver:
      folder: android            # ~/myproject/android/app-release.apk
  ios:
    platform: ios
    steps: [flutter build ipa]
    artifacts: [build/ios/ipa/*.ipa]
    deliver:
      folder: ios/{branch}       # ~/myproject/ios/main/Runner.ipa
  web:
    platform: web
    steps: [npm ci, npm run build]
    artifacts: [dist]
    deliver:
      folder: web                # ~/myproject/web/dist.zip

The folder is relative (no ~, . or ..) and can use the placeholders above. Each subfolder gets its own SHA256SUMS, a latest folder gets the same subfolder, and a command after upload runs inside it. Google Play, Firebase and GitHub Releases ignore it; webhooks get it as job.folder. Without deliver, put {workflow} in the delivery's folder to split jobs by workflow key.

Your server (SFTP)

Make a key for BuildBakery, ideally for a user that can only write to the delivery folder, and allow it on the server:

ssh-keygen -t ed25519 -N "" -C buildbakery -f buildbakery_deploy
# on the server, as the delivery user:
cat buildbakery_deploy.pub >> ~/.ssh/authorized_keys

Add the server on the Account page (or bakery servers add --host … --user … --key buildbakery_deploy). We log in once and pin the server's host key: if it ever changes, deliveries stop with a clear error instead of sending your builds to an impostor.

/srv/releases/{project}/{batch}/{workflow}
  app-release.aab
  mapping.txt
  SHA256SUMS            # verify with: sha256sum -c SHA256SUMS
/srv/releases/{project}/latest/{workflow}   # with "keep a latest folder"

Files land as NAME.part and are renamed when complete, so scripts watching the folder never pick up half a file. An optional command after upload runs in the folder with BB_DIR, BB_FILES, BB_PROJECT, BB_BATCH, BB_WORKFLOW, BB_PLATFORM, BB_BRANCH, BB_COMMIT and BB_BATCH_URL set; its output shows on the batch page and a non-zero exit marks the delivery failed. Commands need a login with a shell (sftp-only accounts can still receive files).

Buckets (S3, R2, B2…)

Create keys that can write to one bucket: on AWS an IAM user with s3:PutObject, s3:GetObject, s3:DeleteObject and s3:ListBucket; on Cloudflare R2 an API token with Object Read & Write for the bucket. Files go under the delivery's prefix with a SHA256SUMS, and optionally a latest/ copy. If the bucket is public (an R2 custom domain, CloudFront…), add its public URL and batches link to the files.

Google Play

  1. In Google Cloud console, create a service account and a JSON key, and add it on the Account page (bakery accounts add google key.json).
  2. In Play Console → Users and permissions, invite the service account's email with “Release apps to testing tracks” (and “Release to production” if you'll use it) for your apps.
  3. Add a Google Play delivery with the package name and track. The .aab is preferred over an .apk; a mapping.txt artifact is attached for crash deobfuscation.

Play only accepts Draft releases for an app that has never been published, and the first build of a new app must be uploaded by hand in Play Console. Version codes must increase every upload, e.g. --build-number=$CB_BUILD_NUMBER for Flutter.

Firebase App Distribution

Uses the same Google service account; give it the Firebase App Distribution Admin role in the Firebase project's IAM. Set the Firebase App ID (Project settings → Your apps), tester group aliases and/or emails. Android uploads the .apk (or an .aab if the app is linked to Play); iOS uploads the .ipa (ad hoc or development signed).

GitHub Release

Uses a GitHub account linked under Git accounts (accounts connected with “Connect GitHub” can write; a token needs Contents: write). The release is created for the tag (e.g. v1.{batch}) at the batch's commit, as a draft unless you turn that off, and the files are attached. Delivering again replaces the assets.

Webhooks

Each finished job POSTs JSON to your URL. Download links work without signing in for 7 days. Check the signature with the secret shown when you added the webhook:

POST /your/endpoint
X-BuildBakery-Event: artifacts
X-BuildBakery-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the secret>

{
  "event": "artifacts",
  "project": { "id": "…", "name": "Shop" },
  "batch": { "number": 12, "url": "https://buildbakery.com/#/builds/…", "branch": "main", "commit": "1a2b3c…", "message": "…" },
  "job": { "workflow": "android", "platform": "android", "status": "success", "folder": "android" },
  "artifacts": [
    { "name": "app-release.aab", "size": 31457280, "sha256": "…", "url": "https://buildbakery.com/api/artifacts/…/download?exp=…&sig=…" }
  ],
  "linksExpireAt": "2026-11-10T12:00:00.000Z"
}
// Node.js: verify the signature
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-buildbakery-signature']))) reject();

Keeping artifacts

By default artifacts are kept forever. In Settings → Storage (or bakery retention --days 30) a project can clear artifacts older than N days and/or keep only the newest N batches. The newest fresh batch always keeps its artifacts, and copies already delivered elsewhere are never touched. Cleared batches also drop their uploaded source, so Bake again only works for batches baked from git.

Ingredients and secrets

Project Ingredients become environment variables in every step. Mark keys, passwords and tokens as secret: they're encrypted at rest, never shown again, and masked as ******** in logs.

Other binary files (a Google services JSON, a certificate) can go in base64 and be decoded in a step. For Android keystores, use Android signing instead.

# once, from your machine
bakery env set GOOGLE_SERVICES_JSON --file app/google-services.json --base64 --secret

# in the recipe
steps:
  - name: google-services.json
    script: echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json

Built-in variables

CB_BUILD_NUMBERBatch number in this project (great for versionCode / CFBundleVersion)
CB_BUILD_ID, CB_JOB_IDUnique ids of the batch and this job
CB_WORKFLOW, CB_PLATFORMThis workflow’s key and platform
CB_BRANCH, CB_COMMITGit branch and commit (when known)
CB_PROJECT_NAME, CB_PROJECT_IDThe project
CB_SOURCE_DIRAbsolute path of the source root
CB_ARTIFACTS_DIRAnything copied here is kept as an artifact
CIAlways true

Caching

Dependencies are cached automatically per project, keyed by your lockfiles: Gradle, CocoaPods, Swift packages, npm, yarn, pnpm, Flutter pub and the tool versions from tools:. Unchanged lockfile → instant restore; changed lockfile → the closest previous cache is restored and a fresh one saved. Add your own paths, or turn it off:

cache:
  paths:
    - ~/Library/Developer/Xcode/DerivedData   # "~/..." or relative to the source
  key_files: [ios/Podfile.lock]               # new cache when these change
# cache: false

If a build behaves strangely after a dependency change, use Clear cache in project settings or bakery cache clear.

Android signing

Upload your keystore in the project's Settings → Android signing (or bakery keystore add release.jks --alias upload) with its keystore password, key alias and key password. Then turn signing on in the workflow:

workflows:
  android:
    platform: android
    android_signing: true          # or { keystore: upload } if the project has several
    steps:
      - ./gradlew bundleRelease

Every step of that workflow then has:

CB_KEYSTORE_PATHAbsolute path of the keystore file on the oven
CB_KEYSTORE_PASSWORDKeystore password (masked in logs)
CB_KEY_ALIASKey alias
CB_KEY_PASSWORDKey password (masked in logs)

A Set up Android signing step runs first and checks the keystore opens, the alias exists and the key password unlocks the key, so a typo fails in seconds instead of at the end of a Gradle build. Point your release build at the variables:

// app/build.gradle
android {
    signingConfigs {
        release {
            if (System.getenv("CB_KEYSTORE_PATH")) {
                storeFile file(System.getenv("CB_KEYSTORE_PATH"))
                storePassword System.getenv("CB_KEYSTORE_PASSWORD")
                keyAlias System.getenv("CB_KEY_ALIAS")
                keyPassword System.getenv("CB_KEY_PASSWORD")
            }
        }
    }
    buildTypes {
        release { signingConfig signingConfigs.release }
    }
}
// app/build.gradle.kts
android {
    signingConfigs {
        create("release") {
            System.getenv("CB_KEYSTORE_PATH")?.let {
                storeFile = file(it)
                storePassword = System.getenv("CB_KEYSTORE_PASSWORD")
                keyAlias = System.getenv("CB_KEY_ALIAS")
                keyPassword = System.getenv("CB_KEY_PASSWORD")
            }
        }
    }
    buildTypes {
        getByName("release") { signingConfig = signingConfigs.getByName("release") }
    }
}

Flutter projects usually read android/key.properties; write it in a step instead of changing Gradle:

steps:
  - name: key.properties
    script: |
      cat > android/key.properties <<EOF
      storePassword=$CB_KEYSTORE_PASSWORD
      keyPassword=$CB_KEY_PASSWORD
      keyAlias=$CB_KEY_ALIAS
      storeFile=$CB_KEYSTORE_PATH
      EOF
  - flutter build appbundle

Keep your own backup of the keystore: Google Play needs the same upload key for every update.

iOS and macOS code signing

Connect an App Store Connect API key on Account → Integrations → Apple Developer Portal, then turn signing on per workflow:

code_signing:
  distribution: app_store      # development | ad_hoc | app_store | enterprise (iOS) | developer_id (macOS)
  bundle_id: com.example.app
  # key: "Team key"            # if you added several keys

A Set up code signing step runs first: it fetches (or creates) the certificate and provisioning profiles for that bundle id, installs them in a fresh keychain, and points your Xcode project at them. Your own steps then just build.

Publishing to the stores

Google Play, Firebase and GitHub Releases are deliveries, set up in the project's settings. App Store Connect / TestFlight uploads run on the Mac oven (Apple's upload tools only run on macOS), as the last step of the workflow:

workflows:
  ios:
    platform: ios
    code_signing: { distribution: app_store, bundle_id: com.acme.shop }
    steps:
      - flutter build ipa --release --build-number=$CB_BUILD_NUMBER --export-options-plist=$HOME/export_options.plist
    artifacts: [build/ios/ipa/*.ipa]
    publish:
      app_store_connect:
        submit_to_testflight: true   # beta review, needed for external testers
        beta_groups: [QA team]       # optional TestFlight groups
        # path: build/ios/ipa/*.ipa  # default: the first .ipa (.pkg for macos) found
        # key: Main key              # which Apple key, if you have several

It uses the App Store Connect key from Integrations. Without submit_to_testflight the build is uploaded and appears in TestFlight for internal testers once Apple has processed it. Build numbers must increase with every upload.

Steps can also use the connected accounts directly:

integrations:
  app_store_connect: true   # APP_STORE_CONNECT_ISSUER_ID / _KEY_IDENTIFIER / _PRIVATE_KEY
  partner_center: true      # MS_STORE_ACCESS_TOKEN (temporary) + MS_PARTNER_CENTER_* ids

The Microsoft client secret never reaches the oven; steps get a short-lived token.

Automation and release safety

  • Triggers: bake every push or pull request (GitHub, GitLab, Bitbucket), report commit statuses, cancel superseded batches, or bake on a schedule. Project → Triggers, or bakery triggers.
  • Why did this burn? AI reads the failed job's log and recipe and suggests a fix (and a corrected recipe). On the batch page, or bakery why.
  • Install previews: QR install pages for APKs and IPAs, and pull request comments.
  • Insights: build times, success rate, waits and cache hits per workflow (bakery insights).
  • Signed provenance: a signed SLSA attestation of what each job built (bakery provenance).
  • MCP server at /api/mcp, on every plan: AI coding agents can bake, read logs and failure analyses, check and re-send deliveries, and list accounts and ingredients (bakery docs automation lists the tools).

Details: bakery docs automation. Some of these depend on your plan (bakery plan).

Ready-made recipes

These build each kind of app. To ship them (your server, Cloudflare, TestFlight, Google Play, testers, GitHub Releases), see the deploying examples.

Android (Gradle)

workflows:
  android:
    platform: android
    android_signing: true            # keystore from Settings → Android signing
    steps:
      - name: Build
        script: ./gradlew bundleRelease assembleRelease -PversionCode=$CB_BUILD_NUMBER
    artifacts:
      - app/build/outputs/**/*.aab
      - app/build/outputs/**/*.apk

iOS (Xcode) to TestFlight

workflows:
  ios:
    platform: ios
    code_signing: { distribution: app_store, bundle_id: com.example.app }
    integrations: { app_store_connect: true }
    steps:
      - name: Pods
        script: pod install
      - name: Build IPA
        script: |
          xcode-project build-ipa --workspace App.xcworkspace --scheme App
      - name: TestFlight
        script: app-store-connect publish --path build/ios/ipa/*.ipa --testflight
    artifacts: [build/ios/ipa/*.ipa]

Flutter (iOS + Android)

workflows:
  android:
    platform: android
    android_signing: true
    steps:
      - flutter pub get
      - name: key.properties
        script: |
          printf 'storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=%s\n' \
            "$CB_KEYSTORE_PASSWORD" "$CB_KEY_PASSWORD" "$CB_KEY_ALIAS" "$CB_KEYSTORE_PATH" > android/key.properties
      - flutter build appbundle --build-number=$CB_BUILD_NUMBER
    artifacts: [build/app/outputs/bundle/release/*.aab]
  ios:
    platform: ios
    code_signing: { distribution: app_store, bundle_id: com.example.app }
    steps:
      - flutter pub get
      - name: Build IPA
        script: |
          flutter build ipa --build-number=$CB_BUILD_NUMBER \
            --export-options-plist=$HOME/export_options.plist
    artifacts: [build/ios/ipa/*.ipa]

The signing step writes ~/export_options.plist for the profile it installed (via xcode-project use-profiles).

React Native

workflows:
  android:
    platform: android
    android_signing: true
    steps:
      - npm ci
      - name: Bundle
        working_directory: android
        script: ./gradlew bundleRelease
    artifacts: [android/app/build/outputs/**/*.aab]
  ios:
    platform: ios
    code_signing: { distribution: ad_hoc, bundle_id: com.example.app }
    steps:
      - npm ci
      - name: Pods
        working_directory: ios
        script: pod install
      - name: Build IPA
        script: xcode-project build-ipa --workspace ios/App.xcworkspace --scheme App
    artifacts: [build/ios/ipa/*.ipa]

macOS app (Developer ID)

workflows:
  mac:
    platform: macos
    code_signing: { distribution: developer_id, bundle_id: com.example.mac }
    steps:
      - name: Archive
        script: |
          xcodebuild -scheme MacApp -configuration Release \
            -archivePath "$CB_WORK_DIR/MacApp.xcarchive" archive
          cp -R "$CB_WORK_DIR/MacApp.xcarchive/Products/Applications/"*.app "$CB_ARTIFACTS_DIR/"

Linux binaries (Go / Rust)

workflows:
  linux:
    platform: linux
    steps:
      - go test ./...
      - name: Build
        script: |
          mkdir -p dist
          GOOS=linux GOARCH=amd64 go build -o dist/app-linux-amd64 .
          GOOS=linux GOARCH=arm64 go build -o dist/app-linux-arm64 .
    artifacts: [dist/*]

Web app

workflows:
  web:
    platform: web
    steps:
      - npm ci
      - npm test -- --ci
      - npm run build
      - name: Package
        script: tar czf "$CB_ARTIFACTS_DIR/site.tar.gz" -C dist .

Troubleshooting

  • "Proofing until a … oven is free": no online oven matches the workflow's platform and labels yet. Check the labels, or ask your admin about oven capacity.
  • "No … oven is available": nothing registered can ever run this job (for example iOS without a Mac oven). Add one, then Keep waiting (or bakery keep-waiting 12 --job ios), or cancel just that job (bakery cancel 12 --job ios).
  • Installing tools failed: a tools: version (or one pinned in .nvmrc etc.) doesn't exist. The log shows mise's message.
  • Invalid YAML: the error shows the line and column. The usual cause is an unquoted : inside a step.
  • "No build instructions": there's no codebuilder.yaml in the source root and no project recipe.
  • Works locally, burns on the oven: run bakery logs --job NAME; check that secrets are set as Ingredients and tools are in the preinstalled list.