Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
push:
branches: [main]
pull_request:

# The workflow only checks out code and runs tests; it never needs write access.
permissions:
contents: read

jobs:
ci:
runs-on: ubuntu-latest

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

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.3'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build production bundle
run: npm run build

- name: Install Playwright Chromium
run: npx playwright install chromium --with-deps

- name: Run browser tests
run: npm run test:ui

- name: Run deploy guard
run: npm run test:deploy-guard

- name: Run proxy allowlist tests
run: npm run test:proxies

- name: Scan production bundle for API key/endpoint leaks
run: python3 scripts/scan_bundle_for_leaks.py
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
46 changes: 33 additions & 13 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,34 +79,54 @@ in `.dart` untouched so a re-deploy is a no-op and an editor rename survives.

Before writing anything to the project the runner compiles the generated
classes in a throwaway Flutter package built from the project's own dependency
versions and refuses the deploy (HTTP 422) if the analyzer reports an error.
versions, and refuses the deploy (HTTP 422) if the analyzer reports an error.
The FlutterFlow DSL only checks that code is formattable, which accepts a call
to a named argument the package never declared. This needs the Flutter SDK in
the image, so the runner takes longer to build and to cold-start, and the
deploy script raises Cloud Run's request timeout to 900s.

A class that imports FlutterFlow scaffolding (`/backend/schema/structs/index.dart`)
is compiled too: the deploy gathers the transitive closure of each class's
project-relative imports from the export and ships it alongside, bounded by
file and byte caps. The class is written to `lib/custom_code/<stem>.dart` -
where FlutterFlow files it, so a relative import resolves from the right
directory - and the project's generated Dart under `lib/`, which is what the
leading slash resolves against. Analysis is scoped to the deployed class files
with `--no-fatal-warnings`, so an error inside the project's own scaffolding,
or a warning-only class, cannot refuse code that compiles.
A class is compiled when every package it imports resolves the same way the
project resolves it. Two cases are left out of the compile set, and each is
reported rather than silently dropped:

* It imports something FlutterFlow only generates once the app is built -
`/backend/schema/structs/index.dart`, `../flutter_flow/lat_lng.dart`. Those
files do not exist until after the deploy.
* It imports a package the project declares from a source this manifest cannot
express: a `git:` or `path:` source, or a nested `hosted:` block. The scratch
package would resolve different code, and a check against the wrong version
reports a result that does not describe what ships.

The second case is decided **per class**, from that class's own imports. A
`git:` dependency stops only the classes that import it; classes that do not are
still compiled. Deciding it once for the whole project meant one such entry left
an entire deploy uncompiled.

**Which packages the Flutter SDK supplies is read from the project's own
pubspec** - a dependency written in block form under `sdk:` is an SDK package by
definition. Nothing keeps a list of SDK package names, because a list goes stale
the moment the SDK ships one it has not heard of, and a stale list reads a
perfectly valid package as unreproducible. That is not hypothetical: a list
missing `flutter_web_plugins` refused whole deploys for projects that declared
it.

The manifest is sent as package names and version constraints, never as
pubspec.yaml text, and the runner builds the document itself - it runs
`pub get` against it on a publicly reachable route, so a caller must not be
able to name a git or path source. `dependency_overrides` are carried over,
because a scratch package resolving different versions than the project would
approve code the project rejects.
approve code the project rejects. The runner validates each name against a
package-name pattern rather than against a list of SDK packages, and writes
`sdk: flutter` itself, so that value never comes from a caller.

A client still sending the older `pubspec` format is recognized, never
executed, and its deploy proceeds with the check reported as unavailable.

**Nothing unverified is deployed.** The web app refuses the deploy on any class
it cannot compile, naming each one and why, before the runner is called.
**What could not be verified still deploys, and is reported.** A class with a
real analyzer error refuses the deploy - HTTP 422, naming the error. A class the
deploy could not compile, for either reason above, does not: it is pushed, and
the warning naming it is logged *before* the provisioning request, so it
describes the mutation before it happens rather than after.

The runner streams its progress. When the request body sets `"stream": true`
the response is NDJSON — one `{"event":"phase"|"log"|"result"}` object per
Expand Down
28 changes: 25 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2113,7 +2113,7 @@ async function provisionMissingCodeFiles(
) {
const missingCodeFiles = findMissingCodeFiles(fileMap, remoteFiles);
if (missingCodeFiles.length === 0) {
return { remoteFiles, syncFileMap: fileMap, unverified: [] };
return { remoteFiles, syncFileMap: fileMap, unverified: [], approximate: [] };
}

// The runner compiles these against the project's own package versions
Expand All @@ -2137,6 +2137,14 @@ async function provisionMissingCodeFiles(
const unverified = verificationPlan.skipped.map((entry) => entry.reason);
unverified.forEach((reason) => console.warn(`[custom class deploy] ${reason}`));

// Classes that do not import an unrepresentable package directly are still
// checked, but against a scratch graph that omits it. That approximation is
// disclosed here - before the push - and returned alongside `unverified`,
// so it reaches the deploy result and is never mistaken for a full-graph
// check.
const approximate = verificationPlan.approximate;
approximate.forEach((notice) => console.warn(`[custom class deploy] ${notice}`));

console.log(
`Provisioning ${missingCodeFiles.length} new FlutterFlow custom code file(s) before sync.`,
);
Expand Down Expand Up @@ -2178,6 +2186,7 @@ async function provisionMissingCodeFiles(
remoteFiles,
syncFileMap: excludeProvisionedCodeFiles(fileMap, missingCodeFiles),
unverified,
approximate,
};
}

Expand Down Expand Up @@ -2870,6 +2879,7 @@ async function commitToFlutterFlow(dartCode, fileName, options = {}) {
message: `Successfully committed ${fileName} to FlutterFlow project ${projectId}`,
addedDependencies: pubspecMerge.added,
unverified: provisioning.unverified,
approximate: provisioning.approximate,
warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [],
};
} catch (error) {
Expand Down Expand Up @@ -3043,6 +3053,7 @@ async function executeCommit(code, options = {}) {
metadata,
addedDependencies: pubspecMerge.added,
unverified: provisioning.unverified,
approximate: provisioning.approximate,
warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [],
elapsedTime: commitState.getElapsedTime(),
};
Expand Down Expand Up @@ -3207,6 +3218,7 @@ async function executeBundleCommit(bundlePlan, options = {}) {
metadata,
addedDependencies: pubspecMerge.added,
unverified: provisioning.unverified,
approximate: provisioning.approximate,
warnings: result.errorMap ? Array.from(result.errorMap.entries()) : [],
elapsedTime: commitState.getElapsedTime(),
};
Expand Down Expand Up @@ -5347,14 +5359,24 @@ function showCommitSuccessModal(result) {
const warningsSection = document.getElementById("success-warnings-section");
const warningsList = document.getElementById("success-warnings-list");
// A class that could not be compiled before the push is reported here rather
// than left implicit, so "deployed" never reads as "checked".
// than left implicit, so "deployed" never reads as "checked". A class that
// was compiled against a reduced package graph is reported beside it: the
// check ran, but approximately, and that has to stay visible too.
const unverified = result.unverified || [];
const approximate = result.approximate || [];
const fileWarnings = result.warnings || [];
if ((fileWarnings.length > 0 || unverified.length > 0) && warningsSection && warningsList) {
if (
(fileWarnings.length > 0 || unverified.length > 0 || approximate.length > 0) &&
warningsSection &&
warningsList
) {
warningsList.innerHTML = [
...unverified.map((reason) =>
`<li><span class="font-medium">Not verified before deploying:</span> ${escapeHtml(String(reason))}</li>`
),
...approximate.map((notice) =>
`<li><span class="font-medium">Verified approximately:</span> ${escapeHtml(String(notice))}</li>`
),
...fileWarnings.map(([file, errs]) =>
`<li><span class="font-medium">${escapeHtml(file)}:</span> ${escapeHtml(String(errs))}</li>`
),
Expand Down
29 changes: 16 additions & 13 deletions cloud-run/ffai-runner/bin/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ final _packageNamePattern = RegExp(r'^[a-z_][a-z0-9_]*$');
// unrepresentable rather than merely discouraged.
final _constraintPattern = RegExp(r'^[A-Za-z0-9^~<>=*+._ -]+$');

// The packages the Flutter SDK supplies, i.e. the only valid `sdk: flutter`
// entries besides `flutter` itself.
const allowedSdkPackages = <String>{
'flutter',
'flutter_test',
'flutter_driver',
'flutter_localizations',
'integration_test',
};
// Which packages the Flutter SDK supplies is not decided here. A caller names
// the packages its project declares as `sdk: flutter`, and this runner emits
// them as `name: {sdk: flutter}` - a value it writes itself, never one a caller
// sent. There was an allowlist of SDK package names here, and it refused good
// deploys: the client classified a package correctly by its pubspec source, and
// the runner rejected it for not appearing on a list that had gone stale.
// `_packageNamePattern` is what actually keeps a key from carrying YAML.

Future<void> main() async {
final port = int.tryParse(Platform.environment['PORT'] ?? '') ?? 8080;
Expand Down Expand Up @@ -538,8 +536,13 @@ Map<String, String> _normalizeDependencyMap(Object? value, String field) {
return result;
}

/// Reads the SDK-supplied package names. Restricted to the packages the Flutter
/// SDK actually provides, so a caller cannot request an arbitrary `sdk:` value.
/// Reads the SDK-supplied package names a caller's project declares.
///
/// Validated as package names rather than against a list of the packages the
/// Flutter SDK happens to ship: a name the SDK does not provide fails
/// `pub get` with an honest error, whereas a list refuses correct deploys the
/// moment it falls behind the SDK. The pattern is the load-bearing check - each
/// name becomes a key in the generated pubspec.
List<String> _normalizeSdkPackages(Object? value) {
if (value == null) return const <String>[];
if (value is! List) {
Expand All @@ -549,8 +552,8 @@ List<String> _normalizeSdkPackages(Object? value) {
final result = <String>[];
for (final raw in value) {
final name = '$raw';
if (!allowedSdkPackages.contains(name)) {
throw FormatException('Unsupported SDK package: $name.');
if (!_packageNamePattern.hasMatch(name)) {
throw FormatException('Invalid SDK package name: $name.');
}
if (!result.contains(name)) result.add(name);
}
Expand Down
320 changes: 0 additions & 320 deletions dist/assets/index-BAqMCFqg.js

This file was deleted.

320 changes: 320 additions & 0 deletions dist/assets/index-ZsXrM4ik.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2420,7 +2420,7 @@
.pm-cards { padding: 0 16px; }
}
</style>
<script type="module" crossorigin src="/assets/index-BAqMCFqg.js"></script>
<script type="module" crossorigin src="/assets/index-ZsXrM4ik.js"></script>
</head>
<body>
<div class="app-shell">
Expand Down
Loading
Loading