Pull, apply, conflicts, and GitHub Actions.

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 apply writes drafts. fcc publish snapshots 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 planfcc 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:

  1. Detects that local and remote both diverged from base.
  2. Saves the remote version next to yours as flows/x.remote.json so you can diff it.
  3. Skips the conflicting file and exits 3 so CI fails. The CLI always prints (and --json includes) the exact relativePath of each conflict — use this in CI summaries instead of relying on the count alone.

Commit sync-state.json after resolve. fcc resolve updates .fetchcatch/sync-state.json with reconciled base hashes. If you commit only the flow (or api-source) file but not sync-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.

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.

Full workflow file →

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