Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e5778bc
fix(deploy): bump pinned flutterflow_cli to 0.0.41 in the ffai runner…
sgardoll Oct 4, 2026
5231e76
fix(deploy): stop the 120s UI bound firing on routine deploys and rep…
sgardoll Oct 4, 2026
6746077
test(e2e): cover the slow-runner timeout and the reconcile message
sgardoll Oct 4, 2026
b16887d
infra(runner): keep the Cloud Run runner warm, deploy it from CI, and…
sgardoll Oct 4, 2026
d55eb19
feat(runner): surface liveness, scale the deploy cap, expose a health…
sgardoll Oct 4, 2026
486c239
fix(deploy): let an unconfirmed deploy whose classes landed finish wi…
sgardoll Oct 4, 2026
6141102
fix(canary): send a sources array so the runner accepts the dry-run p…
sgardoll Oct 4, 2026
6bde72f
fix(runner): make PR 114 independently deployable and keep the node s…
sgardoll Oct 4, 2026
0350ce3
Merge pull request #112 from sgardoll/hardening/e2e-timeout-path
sgardoll Oct 4, 2026
27b7052
Merge pull request #114 from sgardoll/hardening/runner-liveness
sgardoll Oct 4, 2026
fbb4d83
Merge pull request #115 from sgardoll/hardening/deploy-durability
sgardoll Oct 4, 2026
19e90fa
fix(ci): gate the runner workflows on a step, not a job-level secret if
sgardoll Oct 4, 2026
597e9eb
Merge pull request #113 from sgardoll/hardening/infra-keepwarm-and-ca…
sgardoll Oct 4, 2026
862241e
test(e2e): prove an interrupted deploy finishes end-to-end via the re…
sgardoll Oct 4, 2026
2c70ce2
fix(e2e/stu380): account for the reconcile re-read in transport outco…
sgardoll Oct 4, 2026
17aa55a
Merge pull request #116 from sgardoll/hardening/finish-deploy-e2e
sgardoll Oct 4, 2026
bb1b65f
test(runner): guard the image layout and the healthz version lockstep
sgardoll Oct 4, 2026
37fe7ac
ci: run the runner's Dart analyze and tests on every PR
sgardoll Oct 4, 2026
dab59e9
chore(runner): ignore dart pub get output
sgardoll Oct 4, 2026
81db6c5
Merge pull request #117 from sgardoll/fix/stu380-reconcile-read-counts
sgardoll Oct 4, 2026
4dd30a1
fix(deploy): keep the cause when reconciling, and stop crashing witho…
sgardoll Oct 4, 2026
30de965
feat(runner): record deploy runs and serve their status so a lost out…
sgardoll Oct 4, 2026
151b7e2
Merge pull request #118 from sgardoll/hardening/deploy-run-status
sgardoll Oct 4, 2026
bf71e97
fix(runner): close the run record when a deploy throws unexpectedly
sgardoll Oct 4, 2026
373d29a
fix(runner): bound the run-status read, which held the instance at co…
sgardoll Oct 4, 2026
759a511
test(e2e): prove the run-status wiring changes a timed-out deploy's o…
sgardoll Oct 4, 2026
8d2c91c
Merge pull request #119 from sgardoll/run-status-wiring-e2e
sgardoll Oct 4, 2026
d5991e5
fix(run-status): never mint a run id from Math.random
sgardoll Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Runner canary

# A scheduled dry-run smoke test against the production FlutterFlow AI runner,
# so a broken runner is caught the next morning instead of by a user's deploy.
# Runs daily and can be triggered manually with workflow_dispatch.
#
# Additive only: it never deploys and never touches this repository.

on:
schedule:
# 07:21 UTC daily (off the :00/:30 marks to spread GHA load).
- cron: "21 7 * * *"
workflow_dispatch:

permissions:
contents: read

jobs:
canary:
runs-on: ubuntu-latest
steps:
# Skip (rather than fail noisily) until the FlutterFlow API key secret
# exists, so scheduled runs before one-time setup complete are quiet. The
# gate has to be a step: the `secrets` context is not available in a
# job-level `if`, and using it there makes the workflow file invalid. The
# script itself also fails clearly if the key ever comes back empty.
- name: Check for canary credentials
id: creds
env:
CANARY_KEY: ${{ secrets.CANARY_FF_API_KEY }}
run: |
if [ -n "$CANARY_KEY" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::CANARY_FF_API_KEY is not set, so the canary did not run. See DEPLOYMENT.md for the one-time setup."
fi

- name: Checkout
if: steps.creds.outputs.present == 'true'
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up Node.js
if: steps.creds.outputs.present == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: ".nvmrc"

- name: Run the dry-run canary
if: steps.creds.outputs.present == 'true'
run: node scripts/canary-deploy.mjs
env:
CANARY_FF_API_KEY: ${{ secrets.CANARY_FF_API_KEY }}
CANARY_RUNNER_URL: ${{ vars.CANARY_RUNNER_URL || '' }}
CANARY_PROJECT_ID: ${{ vars.CANARY_PROJECT_ID || 'automated-test-miedro' }}
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,30 @@ jobs:

- name: Scan production bundle for API key/endpoint leaks
run: python3 scripts/scan_bundle_for_leaks.py

# The runner's request validation is the code that rejected
# flutter_web_plugins and failed every deploy for months, and its tests only
# ran on a developer's machine - so nothing stopped that regression returning.
# The image is built only at deploy time, so this is also the only place the
# runner is compiled before it ships.
runner:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up Dart
uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d # v1

- name: Install runner dependencies
working-directory: cloud-run/ffai-runner
run: dart pub get

- name: Analyze the runner
working-directory: cloud-run/ffai-runner
run: dart analyze

- name: Test the runner
working-directory: cloud-run/ffai-runner
run: dart test
68 changes: 68 additions & 0 deletions .github/workflows/deploy-runner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Deploy runner

# Deploy the FlutterFlow AI runner to Cloud Run whenever cloud-run/** changes
# land on main, so a merged runner fix actually ships instead of sitting in the
# repo. Additive only: it never touches branch protection and never runs on
# pull requests.

on:
push:
branches: [main]
paths:
- "cloud-run/**"

# The deploy script needs `gcloud` (auth + setup-gcloud below). No write access
# to this repository itself is required.
permissions:
contents: read

concurrency:
group: deploy-runner
cancel-in-progress: true

jobs:
deploy-runner:
runs-on: ubuntu-latest
steps:
# The secret cannot gate the job itself: the `secrets` context is not
# available in a job-level `if` (only github/needs/vars/inputs are), and
# using it there makes the whole workflow file invalid. It is available to
# a step, so the gate is a first step that reports whether the key exists
# and every step that needs it skips while it does not. The intent is
# unchanged: pushes before one-time setup complete are skipped quietly
# rather than failing.
- name: Check for deploy credentials
id: creds
env:
SA_KEY_JSON: ${{ secrets.CCC_FFAI_RUNNER_SA_KEY_JSON }}
run: |
if [ -n "$SA_KEY_JSON" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::CCC_FFAI_RUNNER_SA_KEY_JSON is not set, so the runner was not deployed. See DEPLOYMENT.md for the one-time setup."
fi

- name: Checkout
if: steps.creds.outputs.present == 'true'
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Authenticate to Google Cloud
if: steps.creds.outputs.present == 'true'
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
with:
# JSON of a service account with roles/run.admin + roles/cloudbuild.builds.editor + roles/iam.serviceAccountUser.
credentials_json: ${{ secrets.CCC_FFAI_RUNNER_SA_KEY_JSON }}

- name: Set up gcloud
if: steps.creds.outputs.present == 'true'
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2
with:
project_id: ${{ vars.CCC_PROJECT_ID || 'low-code-connect' }}

- name: Deploy the FlutterFlow AI runner
if: steps.creds.outputs.present == 'true'
run: |
PROJECT_ID="${{ vars.CCC_PROJECT_ID || 'low-code-connect' }}" \
REGION="${{ vars.CCC_REGION || 'us-west1' }}" \
./scripts/deploy_cloud_run_ffai.sh
52 changes: 52 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,58 @@ Deploy the runner with:
PROJECT_ID=low-code-connect REGION=us-west1 ./scripts/deploy_cloud_run_ffai.sh
```

Keep one instance warm by default: the script passes `--min-instances 1`, because
most of a cold start (FlutterFlow AI SDK download and a fresh `flutter pub get`)
is paid once per container instead of once per deploy. Override the count with
`MIN_INSTANCES`, e.g. `MIN_INSTANCES=0 ./scripts/deploy_cloud_run_ffai.sh` to
scale to zero and pay per-request cold starts instead.

#### One-time setup for automated deploys (GitHub Actions)

The workflow `.github/workflows/deploy-runner.yml` deploys the runner whenever
`cloud-run/**` changes on `main`. It uses a Google Cloud service-account key
from a repository secret. Until that secret exists the job **silently skips**
(rather than failing on every push), so set up this once or merged runner
changes will deploy nothing:

1. Create a service account with at least `roles/run.admin`,
`roles/cloudbuild.builds.editor` and `roles/iam.serviceAccountUser`, generate
a JSON key, and download it.
2. In **Settings → Secrets and variables → Actions**, create a repository secret
named `CCC_FFAI_RUNNER_SA_KEY_JSON` whose value is the key's full JSON
(including the surrounding `{ }`).
3. The workflow reads `CCC_PROJECT_ID` (default `low-code-connect`) and
`CCC_REGION` (default `us-west1`) from repository variables; create those if
the defaults are not right. This is per-repo like every secret here, because
GitHub does not expose organisation secrets to private repositories on the
free plan.

#### Runner canary (scheduled)

The workflow `.github/workflows/canary.yml` runs daily (07:21 UTC) and on demand
via **Actions → Runner canary → Run workflow**. It sends a `dryRun: true`
deploy for the throwaway project `automated-test-miedro` to the runner's
`/deployCustomClasses`, so it exercises workspace init, the compile gate and the
DSL push path without writing anything to a real project. It fails (non-zero)
on any non-200 response, on `success:false`, or if the runner rejects the
`flutter_web_plugins` SDK package, and also reports the runner's `/healthz`
versions when that endpoint is deployed - without failing when it is not yet.

`GET /healthz` answers `{sha, flutterflowCli, minFlutterflowCli}`. The deploy
script passes the checkout's short SHA as `RUNNER_GIT_SHA`, so comparing `sha`
against `git log -1 --format=%h` tells you whether a merged runner fix is
actually live. That question is what a months-long stall in a merged-but-never-
deployed fix looked like; it is now one request.

Until the FlutterFlow API key secret exists the canary job skips cleanly, so it
never fails noisily before setup:

1. Create a repository secret named `CANARY_FF_API_KEY` whose value is a
FlutterFlow API key with at least read access to `automated-test-miedro`.
2. Optionally set a repository variable `CANARY_RUNNER_URL` if the production
runner lives somewhere other than the default, and `CANARY_PROJECT_ID` if the
throwaway project changes.

## BuildShip workflow and step prompts

The generation pipeline (Architect → Generator → Review) runs on BuildShip, and
Expand Down
Loading
Loading