Sync and CI
FetchCatch sync is a three-way model — like git, like Terraform. Your repo holds the desired state, the server holds the live state, and the CLI keeps them honest using content hashes.
Sync is not publish
fcc applywrites drafts.fcc publishsnapshots a draft as a numbered, callable version visible to/v1/evaluate. Whether the console, the CLI, or both can publish is controlled by the workspace governance mode — see governance.md.
The three states
For every resource the CLI tracks three hashes:
| Hash | Where | Meaning |
|---|---|---|
BaseHash |
.fetchcatch/sync-state.json |
What the server had the last time we pulled. |
LocalHash |
recomputed from your file | What's on disk right now. |
RemoteHash |
server manifest | What's on the server right now. |
Comparing the three gives us a precise change kind:
| Local vs Base | Remote vs Base | Kind |
|---|---|---|
| same | same | Unchanged |
| changed | same | ModifiedLocal (will push) |
| same | changed | ModifiedRemote (will pull) |
| changed | changed, same as local | Unchanged (you happened to make the same edit) |
| changed | changed, different | Conflict |
| new | absent | NewLocal (will push) |
| absent | new | NewRemote (will pull) |
| absent (file deleted) | same | DeletedLocal (will delete on server) |
| same | absent (removed on server) | DeletedRemote (will delete local file on pull) |
The hash is SHA-256 of canonical (compact, camelCase) JSON. Pretty-printing on disk does not affect it.
Deleting flows
| Where | How |
|---|---|
| Console | Flows list → trash icon → confirm. Run fcc pull to remove local JSON. |
| Git / CLI | Delete (or git rm) .fetchcatch/flows/{slug}.json, then fcc plan → fcc apply. |
Only flows can be deleted via sync apply. Response types and API sources still require the console (response types: Response types page; blocked if flows reference them).
Deleting a flow removes its draft and published versions from the workspace. Historical runs remain in the runs list for audit; /v1/evaluate/{slug} returns 404 afterward.
Commands (the day-to-day loop)
fcc pull # bring local in sync with server (refuses to overwrite conflicts)
fcc plan # preview: what would apply do?
fcc apply # push your changes (writes drafts; refused on DesignerOnly)
fcc publish # snapshot drafts as numbered, callable versions
fcc status # short version of plan
fcc drift # CI-friendly drift report (exit 2 on divergence)
Multi-environment? Add --env <name> to any of the above and configure named environments
in project.json — see environments.md.
When the designer and the CLI both touched the same file:
fcc diff flows/x.json # unified diff of local vs remote
fcc resolve flows/x.json --keep-mine # local wins, overwrite remote
fcc resolve flows/x.json --keep-theirs # remote wins, overwrite local
fcc resolve flows/x.json --edit # open both in $EDITOR, then keep-mine
fcc apply # actually push
And for the "who broke prod" moment:
fcc history flows/x.json # list versions: v1, v2, v3...
fcc rollback flows/x.json --version 3 # restore that version
Exit codes (the contract CI relies on)
| Command | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
fcc plan, fcc status |
in sync | error | pending changes | conflicts |
fcc apply |
applied / nothing to do | error | — | conflicts |
fcc publish |
published / nothing to publish | item errored | — | — |
fcc drift |
clean | error | drift or pending | conflicts |
Strict. Documented. Use them in scripts.
JSON output
Every state command supports --json:
fcc plan --json
fcc status --json
fcc apply --json
fcc history flows/x.json --json
Schema for plan/status/apply (one shape, all fields always present):
{
"inSync": false,
"push": [
{ "type": "flow", "relativePath": "flows/x.json", "kind": "ModifiedLocal" }
],
"pull": [],
"conflicts": [],
"skippedPullOnly": [],
"applied": 1,
"skipped": 0,
"errors": 0,
"items": [
{ "type": "flow", "id": "…", "relativePath": "flows/x.json", "status": "applied", "message": null }
]
}
items is populated only by apply. skippedPullOnly lists local changes the CLI will not push (pull-only metadata such as api-keys/*.json). kind is one of NewLocal, NewRemote, ModifiedLocal, ModifiedRemote, DeletedLocal, DeletedRemote, Conflict.
Pull-only resources
Some workspace files are pulled for reference but cannot be pushed via sync:
| Path | Type | Notes |
|---|---|---|
api-keys/{slug}.json |
api-key |
Metadata only (name, id, created date). Secrets are never synced. Create or revoke keys in the console, then fcc pull. |
If a local api-keys/*.json file exists but the key is not on the server (for example a leftover test file in git), fcc plan and fcc apply list it under skippedPullOnly and skip it instead of failing with a server error.
Conflict resolution
By default, fcc apply is non-destructive on conflict:
- Detects that local and remote both diverged from base.
- Saves the remote version next to yours as
flows/x.remote.jsonso you can diff it. - Skips the conflicting file and exits 3 so CI fails. The CLI always prints (and
--jsonincludes) the exactrelativePathof each conflict — use this in CI summaries instead of relying on the count alone.
Commit
sync-state.jsonafter resolve.fcc resolveupdates.fetchcatch/sync-state.jsonwith reconciled base hashes. If you commit only the flow (or api-source) file but notsync-state.json, the next CI run still sees stale base hashes and may report the same conflict again.
Recovery options in increasing order of severity:
| Path | When | Command |
|---|---|---|
| Pull theirs | Designer edits are correct, drop your local change | fcc resolve flows/x.json --keep-theirs then commit resource + sync-state.json |
| Push mine | Your repo is correct, the designer change was wrong | fcc resolve flows/x.json --keep-mine && fcc apply then commit resource + sync-state.json |
| Push delete | Flow should be removed from the workspace | Delete the local file, then fcc apply (or fcc resolve --keep-mine if you deleted locally during a conflict) |
| Merge manually | Both have good changes | fcc diff flows/x.json, edit the local file by hand, then fcc apply |
--force (CI override) |
You know local must win, every time | fcc apply --force |
--force drops the optimistic-concurrency hash check on the server. Use it only when you've explicitly decided the repo is the single source of truth — typically in a "deploy from main" pipeline that runs after a human-reviewed PR.
Recommended CI
A two-job workflow: plan on PR, apply on merge to main. The plan job posts a markdown summary as a PR comment and fails the check if conflicts exist; the apply job fails if a designer edit landed between PR open and merge so a human can decide between resolve and --force.
Authentication
CLI tokens are issued from Console → Settings → CLI tokens, scoped to a tenant + workspace. In CI:
env:
FETCHCATCH_TOKEN: ${{ secrets.FETCHCATCH_TOKEN }}
CI: "true"
Setting CI=true (which GitHub Actions does for you) auto-disables interactive prompts.
Pin the CLI version
CLI binaries are hosted publicly under fetchcatch.com/downloads. The recommended pattern in CI is to resolve the asset URL through the public manifest — that way the storage backing can change without your workflow breaking:
env:
FCC_VERSION: "0.1.22"
steps:
- run: |
set -euo pipefail
url=$(curl -fsSL https://fetchcatch.com/downloads/manifest.json \
| jq -r --arg v "$FCC_VERSION" \
'.releases[] | select(.version==$v) | .assets[] | select(.platform=="linux-x64") | .url')
curl -fsSL -o /usr/local/bin/fcc "$url"
chmod +x /usr/local/bin/fcc
Drop the select(.version==$v) filter and use .releases[0] to track the latest release automatically. Pinning is safer.
Designer awareness
When you pull from CI or your laptop, the flow designer shows a non-blocking amber banner for ~30 minutes: "alice@… pulled this workspace 4 minutes ago from runner-abc123. Refresh before editing to avoid conflicts." This is a hint, not a lock — the conflict detector is still the safety net.
Best practices
| Do | Avoid |
|---|---|
Commit .fetchcatch/ including sync-state.json |
Committing *.remote.json (it's in .gitignore) |
| Pin CLI version in CI for reproducibility | Editing id fields in flow / response JSON |
Run fcc plan in PR checks; fail on exit 3 |
Editing contentHash by hand |
Use --force only after human review |
Auto-merging conflicts with --force from every push |
Use fcc history / fcc rollback for recovery |
Restoring from git when prod is broken — version history is faster |
Protocol (for tool authors)
The CLI is a thin wrapper over these HTTP endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/sync/manifest |
List resources + content hashes |
GET |
/v1/sync/export |
Pull all documents |
POST |
/v1/sync/apply |
Push changes (with optional expectedRemoteHash) |
GET |
/v1/sync/pull-state |
Last-pulled-by metadata for the designer banner |
GET |
/v1/sync/history/{type}/{id} |
Version list |
GET |
/v1/sync/history/{type}/{id}/{version} |
One version's full document |
POST |
/v1/sync/rollback |
Restore a specific version |