Skip to content

fix: catch unsupported Custom Action return types before deploy - #48

Merged
sgardoll merged 8 commits into
mainfrom
agent/fix-flutterflow-action-returns
Jul 26, 2026
Merged

fix: catch unsupported Custom Action return types before deploy#48
sgardoll merged 8 commits into
mainfrom
agent/fix-flutterflow-action-returns

Conversation

@sgardoll

@sgardoll sgardoll commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Problem

Custom Actions returning a type FlutterFlow cannot represent were reaching FlutterFlow and being rejected at push time with "Unable to process return parameter" — the reported recurring deployment failure (write_nfc_tag.dart, scan_and_execute_nfc_tag.dart).

The first attempt in this branch checked the return type against types declared by class/enum in the same push. Those actions returned NfcTag / NdefMessage, imported from the nfc_manager package and never declared in the push — so the check found nothing, validateFileMap passed, and the deploy went out and failed remotely.

Fix

Check the return type against an allowlist of what FlutterFlow's Custom Action Return Value selector actually accepts, so an unsupported type is caught regardless of where it is defined. This activates the deploy gate that already exists (validateDartFilevalidateFileMap → all three commit paths) rather than adding a second one.

Ways enforcement was silently skipped, all fixed:

Case Before After
Future<NfcTag> (imported type) passed → FF 400 blocked pre-deploy
Future<List<NdefMessage>> (2-deep generic) regex missed it balanced-bracket parse
artifact name "Write NFC Tag" vs writeNfcTag check skipped entirely name-normalized match
class Result in a doc comment polluted declared types comments/strings stripped
class FooStruct/FooRecord declared in the same push suffix match let it through unconditionally only allowed when not declared in this push

A private helper (Future<NfcTag> _readTag()) declared above the action is no longer mistaken for the entry point, which would have hard-blocked a valid deploy.

The last row was a code-review finding on this PR: a real FlutterFlow Data Type or Firestore Record is never defined via class X in a pushed Code File — it already exists in the project — so a *Struct/*Record-suffixed name declared in the same push is a local Dart class wearing the naming convention, not the real thing, and still can't be used as a return value.

Allowlist

Verified against FlutterFlow's documented return value types: Integer, Double, Boolean, String (also Image/Video/Audio Path), Color, Document (*Record), Document Reference, JSON, DateTime, TimestampRange (DateTimeRange), the List option, and Data Types (*Struct).

Both no-return spellings (Future, Future<void>) stay allowed so side-effect actions are not blocked — FlutterFlow's own boilerplate emits a bare Future.

Deploy tooling

DEPLOYMENT.md referenced scripts/deploy_ftp.py, which never existed. Added it: uploads dist/, then prunes remote files that no longer exist locally, scoped only to directories dist/ itself manages (root, api/, assets/) — so Vite's content-hashed bundles (index-<hash>.js) don't accumulate on the server the way three prior ones already had. It never touches a remote directory with no local counterpart. --dry-run previews both the upload and prune sets.

Testing

npm test — 90/90 passing, including a regression test reproducing the exact NFC failure, plus deep-generic, display-name, comment-pollution, private-helper, declared-suffix, and full allowlist coverage.

Deployed and verified live at customcode.connectio.com.au: correct bundle serving, fix confirmed present in the shipped code, stale bundles pruned with zero collateral (dry-run checked before every delete).

Notes

  • validateFileMap / validateDartFile live in app.js and are not exported, so the wiring itself is verified by trace and simulation rather than by test.
  • LatLng / FFPlace / FFUploadedFile are allowed but documented only for arguments; left permissive deliberately, since wrongly blocking them would stop a working deploy with no override, whereas allowing them falls back to FlutterFlow's own error.
  • The Cloud Run memory/concurrency change in the first commit addresses a separate OOM symptom, not this 400.

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR strengthens Custom Action return-type validation and updates deployment tooling.

  • Parses nested Future return types and validates them against FlutterFlow-supported return values.
  • Collects Dart declarations across bundles and commit file maps to distinguish pushed classes from existing project types.
  • Adds an FTP deployment and stale-file pruning script.
  • Raises Cloud Run memory and serializes provisioning requests.
  • Rebuilds the generated frontend assets and expands validation coverage.

Confidence Score: 3/5

The PR should not merge until suffix-named imported return types are rejected by the deploy gate.

The nested-generic path is addressed, but an imported package class ending in Struct or Record is absent from declaredTypes and therefore remains accepted as a project type, allowing an unsupported Custom Action return contract to reach FlutterFlow.

Files Needing Attention: src/flutterFlowArtifactValidation.js

Important Files Changed

Filename Overview
src/flutterFlowArtifactValidation.js Adds balanced return-type parsing, action entry-point selection, and allowlist-based compatibility validation.
app.js Integrates return-type validation into file-map commit paths and reduces sensitive request logging.
scripts/deploy_ftp.py Adds uploads, dry-run support, and stale remote-file pruning for built assets.
scripts/deploy_cloud_run_ffai.sh Increases default memory and limits Cloud Run request concurrency.
src/flutterFlowArtifactValidation.test.js Adds regression coverage for nested generics, imported types, display names, private helpers, and declared suffix-named classes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Generated artifact bundle] --> B[Extract declared Dart types]
  B --> C[Parse Custom Action Future return type]
  C --> D{Supported by FlutterFlow?}
  D -->|Yes| E[Continue commit path]
  D -->|No| F[Block before deployment]
Loading

Reviews (3): Last reviewed commit: "build: rebuild dist with suffix-bypass v..." | Re-trigger Greptile

Comment thread src/flutterFlowArtifactValidation.js Outdated
sgardoll and others added 4 commits July 26, 2026 12:57
The return-type check only rejected types declared by `class`/`enum` in the
same push, so an action returning an imported package type (nfc_manager's
NfcTag) or a type already in the project was invisible to it. Those pushes
passed validateFileMap and FlutterFlow rejected them with "Unable to process
return parameter".

Check the return type against the types FlutterFlow's Action Return Value
selector exposes instead, so any unsupported type is caught regardless of
where it is defined. This activates the existing deploy gate in
validateDartFile rather than adding a second one.

Also fixes three ways the check silently skipped enforcement:
- nested generics deeper than one level (Future<List<NdefMessage>>) are now
  parsed with a balanced-bracket scan instead of a regex
- artifact display names ("Write NFC Tag") now match their Dart identifier
  (writeNfcTag) instead of failing to match and skipping the check
- class/enum names appearing only in comments or strings no longer register
  as declared types

Struct Data Types and both no-return spellings (Future, Future<void>) stay
allowed so side-effect actions are not blocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When the artifact name does not match any function, the check fell back to the
first Future signature in the file. A private helper declared above the action
(Future<NfcTag> _readTag()) would then be read as the action's return type and
hard-block a valid deploy. Prefer a public signature, since FlutterFlow never
calls a private function as an action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The allowlist was built from the app's own prompt rules and missed two types
FlutterFlow's Return Value selector actually offers: Document, which generates
as <Collection>Record, and TimestampRange, which generates as DateTimeRange.
Both would have been wrongly blocked before deploy.

Verified the allowlist against the documented selector list (Integer, Double,
Boolean, String, Image/Video/Audio Path, Color, Document, Document Reference,
JSON, DateTime, TimestampRange, plus List and Data Types).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sgardoll sgardoll changed the title Enforce FlutterFlow Custom Action return contracts fix: catch unsupported Custom Action return types before deploy Jul 26, 2026
@sgardoll

Copy link
Copy Markdown
Owner Author

@greptileai

Comment thread src/flutterFlowArtifactValidation.js Outdated
sgardoll and others added 3 commits July 26, 2026 14:54
getUnsupportedReturnTypeIdentifiers treated any identifier ending in "Struct"
or "Record" as an existing FlutterFlow project type, without checking whether
it was actually declared as a class/enum in the same push. A real Data Type or
Firestore Record is never defined via `class X` in a pushed Code File - it
already exists in the project - so a declared name with that suffix is a local
Dart class wearing the naming convention, and FlutterFlow still rejects it as
a return value exactly like an imported type.

Pass declaredTypes into the suffix check: a *Struct/*Record identifier is only
treated as a real project type when it is NOT declared in this push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DEPLOYMENT.md referenced this script, but it never existed - deploys were
done by manually uploading dist/ one-off. Vite content-hashes built assets
(index-<hash>.js), so every rebuild orphaned the previous hash's file on the
server; three such orphans had already accumulated (visible in .gitignore's
history of index-*.js entries).

The script uploads every file in dist/, then prunes any remote file that no
longer exists locally, scoped to only the directories dist/ itself manages
(the account root, api/, assets/). It never lists or touches a remote
directory with no local counterpart, so nothing outside dist's own footprint
can be affected. --dry-run previews both the upload and prune sets without
changing the server.

Password resolves from FTP_PASSWORD if set, else macOS Keychain, matching
the existing DEPLOYMENT.md instructions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gitguardian

gitguardian Bot commented Jul 26, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
35186025 Triggered FTP Credentials 5deb4fd scripts/deploy_ftp.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@sgardoll

Copy link
Copy Markdown
Owner Author

@greptileai

@sgardoll
sgardoll merged commit 58f977f into main Jul 26, 2026
3 of 5 checks passed
@sgardoll
sgardoll deleted the agent/fix-flutterflow-action-returns branch July 26, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant