# 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](./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](./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`:

```bash
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):

```json
{
  "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.

## 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`.

[**Full workflow file →**](https://github.com/teclogist/fetchcatch/blob/main/backend/tools/FetchCatch.Cli/examples/github-actions-sync.yml)

### Authentication

CLI tokens are issued from **Console → Settings → CLI tokens**, scoped to a tenant + workspace. In CI:

```yaml
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`](https://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:

```yaml
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 |
