BuildBakeryRequest access

Deploying: example recipes

Complete codebuilder.yaml recipes for shipping common kinds of apps, each with its one-time setup. Copy one, change the names, and check it with bakery recipe validate.

Artifacts leave BuildBakery in two ways:

  • Deliveries, set up once per project (Settings → Delivery or bakery deliveries add): your server (SFTP), S3/R2 buckets, Google Play, Firebase App Distribution, GitHub Releases and webhooks. They run as each job finishes, filtered by workflow and branch. Options: bakery docs deliveries.
  • Recipe steps: publish: for TestFlight and notarization, and your own commands with secret ingredients (bakery env set NAME --secret) for hosts like Cloudflare, Vercel or your servers.

A successful job with an active delivery publishes. Keep test builds from shipping with --workflows, branch filters on deliveries, or manual: true, and check with bakery deliver BATCH --dry-run.

Web app to your own server (SFTP)

Build a static site or SPA and publish it to a server you control, switching to the new release only when it is complete.

workflows:
  web:
    platform: web
    tools: { node: 22 }
    steps:
      - npm ci
      - npm test -- --ci
      - npm run build
      - name: Package
        script: cd dist && zip -qr "$CB_ARTIFACTS_DIR/site.zip" .

One-time setup

# Once: add the server (SSH key login; its host key is pinned)
bakery servers add --host web.example.com --user deploy --key ~/.ssh/deploy_key
# Once: deliver main-branch builds, unpack them and switch the "current" link
bakery deliveries add sftp --to web.example.com --folder 'releases/{batch}' \
  --files 'site.zip' --workflows web --branches main \
  --command 'unzip -q site.zip -d site && ln -sfn "$BB_DIR/site" ~/www/current'
  • Point the web server (nginx, Caddy…) at ~/www/current. Each release lands in its own folder, so a failed upload never breaks the live site.
  • Builds from other branches still bake (and stay downloadable) but are not delivered.

Web app to Cloudflare Pages (or Vercel, Netlify)

Deploy with the host’s own CLI from a recipe step, using a secret token from ingredients.

workflows:
  web:
    platform: web
    tools: { node: 22 }
    steps:
      - npm ci
      - npm run build
      - name: Deploy
        script: |
          if [ "$CB_BRANCH" = main ]; then
            npx --yes wrangler@3 pages deploy dist --project-name my-site --branch main
          else
            npx --yes wrangler@3 pages deploy dist --project-name my-site --branch "$CB_BRANCH"   # preview URL
          fi

One-time setup

# Once: the API token (Cloudflare → My Profile → API Tokens, "Cloudflare Pages: Edit")
bakery env set CLOUDFLARE_API_TOKEN --secret --stdin    # paste the token, then Ctrl-D
bakery env set CLOUDFLARE_ACCOUNT_ID 0123456789abcdef0123456789abcdef
  • Secret ingredients reach steps as environment variables and are masked in logs.
  • Vercel: `npx --yes vercel deploy --prod --token "$VERCEL_TOKEN"`. Netlify: `npx --yes netlify-cli deploy --prod --dir dist` with NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID.
  • Prefer `manual: true` on the workflow if deploys should only happen when you ask (`bakery bake --workflows web`).

iOS app to TestFlight

Sign with the account’s App Store Connect key, build an .ipa on a Mac oven and upload it to TestFlight.

workflows:
  ios:
    platform: ios
    xcode: "26"
    code_signing: { distribution: app_store, bundle_id: com.example.app }
    steps:
      - name: Pods
        script: pod install
      - name: Build IPA
        script: |
          xcode-project build-ipa --workspace App.xcworkspace --scheme App
    artifacts: [build/ios/ipa/*.ipa]
    publish:
      app_store_connect:
        submit_to_testflight: true      # also submit for external beta review
        what_to_test: New onboarding flow

One-time setup

# Once: connect an App Store Connect API key (Account → Integrations → Apple)
# Once, for external testers: fill in TestFlight test information
#   (review contact on the Apple key; feedback email and demo account on Settings → Signing)
  • Build numbers must increase: set CFBundleVersion from $CB_BUILD_NUMBER (e.g. `agvtool new-version -all $((1000 + CB_BUILD_NUMBER))` in a step) above anything uploaded before.
  • Without submit_to_testflight the build goes to internal testers only.
  • If the upload works but only the beta review submission fails, the job still succeeds with a warning: the build is in TestFlight.

Android app to Google Play

Sign an .aab with the project keystore, check the signature and upload it to a Play track.

workflows:
  android:
    platform: android
    android_signing: { keystore: release }
    verify_signing: { keystore: release }     # fail if signed with any other key
    steps:
      - name: Bundle
        script: ./gradlew bundleRelease -PversionCode=$((1000 + CB_BUILD_NUMBER))
    artifacts:
      - app/build/outputs/bundle/release/*.aab
      - app/build/outputs/mapping/release/mapping.txt

One-time setup

# Once: the upload keystore (you type its passwords; never commit it)
bakery keystore add /full/path/upload.jks --alias upload --name release
# Once: a Google service account with release permission (Account → Integrations → Google Play)
# Once: send main-branch builds to the internal track
bakery deliveries add google-play --to play-uploader --package com.example.app --track internal \
  --workflows android --branches main
# Later, to roll out to production gradually:
#   bakery deliveries set 1 --option track=production --rollout 10
  • The very first build of a new app must be uploaded by hand in Play Console once; apps never published only accept `--status draft`.
  • mapping.txt is attached to the version for readable crash reports.

Test builds to testers (Firebase App Distribution)

Send every build from a branch to a tester group, Android and iOS (ad hoc).

workflows:
  android:
    platform: android
    android_signing: true
    steps:
      - ./gradlew assembleRelease -PversionCode=$CB_BUILD_NUMBER
    artifacts: [app/build/outputs/apk/release/*.apk]
  ios:
    platform: ios
    code_signing: { distribution: ad_hoc, bundle_id: com.example.app }
    steps:
      - pod install
      - xcode-project build-ipa --workspace App.xcworkspace --scheme App
    artifacts: [build/ios/ipa/*.ipa]

One-time setup

# Once per app (the Firebase App IDs are in Project settings → Your apps)
bakery deliveries add firebase --to firebase-uploader --app-id 1:123:android:abc --groups qa \
  --workflows android --branches 'develop,release/*'
bakery deliveries add firebase --to firebase-uploader --app-id 1:123:ios:def --groups qa \
  --workflows ios --branches 'develop,release/*'
  • Ad hoc iOS builds only install on devices registered in the Apple developer account.
  • Pull request builds also get an install link and QR code in the PR comment (Triggers), without Firebase.

Flutter app to TestFlight and Google Play

One recipe for both stores: Android on a Linux oven, iOS on a Mac oven, in parallel.

workflows:
  android:
    platform: android
    android_signing: { keystore: release }
    verify_signing: { keystore: release }
    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=$((1000 + 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
      - flutter build ipa --build-number=$((1000 + CB_BUILD_NUMBER)) --export-options-plist=$HOME/export_options.plist
    artifacts: [build/ios/ipa/*.ipa]
    publish:
      app_store_connect: { submit_to_testflight: true }

One-time setup

bakery keystore add /full/path/upload.jks --alias upload --name release
bakery deliveries add google-play --to play-uploader --package com.example.app --track internal --workflows android --branches main
  • The code signing step writes ~/export_options.plist for the profile it installed.
  • Use the same build number offset on both platforms so versions line up.

React Native / Expo app to TestFlight and Google Play

Generate the native projects with Expo prebuild, then sign and ship each platform.

workflows:
  android:
    platform: android
    tools: { node: 22 }
    android_signing: { keystore: release }
    steps:
      - npm ci
      - npx expo prebuild --platform android --no-install
      - name: Bundle
        working_directory: android
        script: |
          ./gradlew bundleRelease -PversionCode=$((1000 + CB_BUILD_NUMBER)) \
            -Pandroid.injected.signing.store.file="$CB_KEYSTORE_PATH" \
            -Pandroid.injected.signing.store.password="$CB_KEYSTORE_PASSWORD" \
            -Pandroid.injected.signing.key.alias="$CB_KEY_ALIAS" \
            -Pandroid.injected.signing.key.password="$CB_KEY_PASSWORD"
    artifacts: [android/app/build/outputs/bundle/release/*.aab]
  ios:
    platform: ios
    tools: { node: 22 }
    code_signing: { distribution: app_store, bundle_id: com.example.app }
    steps:
      - npm ci
      - npx expo prebuild --platform ios
      - name: Re-apply signing to the generated project
        script: |
          export PATH="$HOME/.local/bin:$PATH"
          xcode-project use-profiles --project 'ios/*.xcodeproj'
      - xcode-project build-ipa --workspace ios/*.xcworkspace --scheme MyApp
    artifacts: [build/ios/ipa/*.ipa]
    publish:
      app_store_connect: { submit_to_testflight: true }

One-time setup

bakery keystore add /full/path/upload.jks --alias upload --name release
bakery deliveries add google-play --to play-uploader --package com.example.app --track internal --workflows android --branches main
  • Signing runs before your steps, so a project generated later (prebuild) needs `xcode-project use-profiles` again.
  • Generated Gradle projects take the keystore through the android.injected.signing properties instead of edits to build.gradle.
  • Template: `bakery recipe init --template android-expo`.

macOS app outside the App Store (notarized, on GitHub Releases)

Sign with Developer ID, notarize with Apple, and attach the .dmg to a GitHub release.

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
          mkdir -p dist && cp -R "$CB_WORK_DIR/MacApp.xcarchive/Products/Applications/MacApp.app" dist/
      - name: Disk image
        script: hdiutil create -volname MacApp -srcfolder dist -ov -format UDZO "MacApp-$CB_BUILD_NUMBER.dmg"
    artifacts: ["MacApp-*.dmg"]
    publish:
      notarize: { path: "MacApp-*.dmg" }    # notarize and staple before artifacts are collected

One-time setup

# Once: deliver main-branch builds as draft releases (publish them on GitHub when ready)
bakery deliveries add github-release --tag 'v1.{batch}' --files '*.dmg' --workflows mac --branches main
  • Notarization needs hardened runtime on and every binary signed with Developer ID; Apple’s log lists anything missing.
  • `--draft=false` publishes releases straight away.

Electron desktop app for macOS, Windows and Linux

Build installers per platform with electron-builder and collect them in one GitHub release.

workflows:
  mac:
    platform: macos
    tools: { node: 22 }
    code_signing: { distribution: developer_id, bundle_id: com.example.desktop }
    steps:
      - npm ci
      - npx electron-builder --mac dmg --publish never
    artifacts: [dist/*.dmg]
    publish:
      notarize: { path: "dist/*.dmg" }
  windows:
    platform: windows
    tools: { node: 22 }
    steps:
      - npm ci
      - npx electron-builder --win nsis --publish never
    artifacts: [dist/*.exe]
  linux:
    platform: linux
    tools: { node: 22 }
    steps:
      - npm ci
      - npx electron-builder --linux AppImage --publish never
    artifacts: [dist/*.AppImage]

One-time setup

bakery deliveries add github-release --tag 'v{batch}' --files '*.dmg,*.exe,*.AppImage' --branches main
  • Windows jobs need a Windows oven of your own (there are no hosted Windows ovens); signing the .exe uses your own certificate in a step.
  • Each platform finishes and is delivered independently; the release collects all three.

Backend or Docker app to a server over SSH

Test and package on BuildBakery, then ship to your server with a deploy key, only when you ask.

workflows:
  deploy:
    platform: linux
    manual: true            # only when named: bakery bake --git --workflows deploy
    tools: { node: 22 }
    steps:
      - npm ci
      - npm test
      - npm run build
      - name: Ship
        script: |
          key="$(mktemp)"; trap 'rm -f "$key"' EXIT; chmod 600 "$key"
          printf '%s\n' "$DEPLOY_SSH_KEY" > "$key"
          tar czf release.tgz dist package.json package-lock.json
          scp -i "$key" -o StrictHostKeyChecking=accept-new release.tgz deploy@app.example.com:releases/
          ssh -i "$key" deploy@app.example.com "./deploy.sh releases/release.tgz"

One-time setup

# Once: the deploy key, from a file (never in the recipe or chat)
bakery env set DEPLOY_SSH_KEY --secret --file /full/path/deploy_key
# Deploy the pushed main branch:
bakery bake --git --branch main --workflows deploy
  • `manual: true` keeps pushes and "all workflows" batches from deploying; only a batch that names the workflow runs it.
  • Pin the server’s host key (a known_hosts file in the repo, StrictHostKeyChecking=yes) once you have it.
  • BuildBakery deploys itself this way.