---
title: PushOutput
description: "The output of a `push` Effect: what the `onDone` of its state gets as `event.output`."
---

# `PushOutput`

Type, in `@jr2/orchestrator/src/sandbox-effect.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Effects.

```ts
type PushOutput =
  | Exclude<PushWireOutput, { outcome: "pushed" }>
  | (Omit<Extract<PushWireOutput, { outcome: "pushed" }>, "bundle"> & { sync: SyncState; syncReason?: string });
```

The output of a `push` Effect: what the `onDone` of its state gets as `event.output`. Every
outcome is output, so route on `outcome`:

- `pushed`: the remote has the commit. `published` is the commit on the remote, and `source` is
  the Agent's commit that it comes from. They are the same commit, unless the push was signed or
  squashed. `previous` is what the remote had before, and `unchanged` is `true` when the remote
  had the commit already. `squashed` is `signed` or `unsigned` when the commit on the remote is
  one squashed commit. `via` tells how jr2 found the new commits after a signed push:
  `ancestry`, `pair`, `cherry` or `force`.
- `rejected`: git or the remote refused the push. `stage` tells where, `reason` tells why, and
  `detail` has the words of git.
- `diverged`: the commit on the remote is not in the history of the Agent's branch, and jr2
  could not pair it with a commit there. Nothing was pushed. `reason` tells why, and `published`
  and `source` name the two commits.
- `unknown`: jr2 cannot tell what the push did. `reason` tells why.

Beside `pushed`, `sync` tells what jr2 did to the Agent's branch after the push:

- `synced`: the commit on the remote is not the Agent's own, and jr2 moved the branch to the
  published commits.
- `already`: the branch has the published commit already.
- `skipped`: the remote has the Agent's own commit, so the branch does not move.
- `busy`: a rebase, merge, cherry-pick or revert of the branch is in progress. Nothing changed.
- `not-synced`: the branch did not move.

With `busy` or `not-synced`, `syncReason` tells why, and jr2 tries again before the next push.

**Forms**: it is one of these objects.

- Form 1: `outcome`, `stage`, `reason`, `detail`

  - `outcome`: `"rejected"`
  - `stage`: `string`
  - `reason`: `string`
  - `detail` (optional): `string`
- Form 2: `outcome`, `reason`, `published`, `source`

  - `outcome`: `"diverged"`
  - `reason`: `string`
  - `published`: `string`
  - `source`: `string`
- Form 3: `outcome`, `reason`

  - `outcome`: `"unknown"`
  - `reason`: `string`
- Form 4: `outcome`, `published`, `source`, `previous`, `via`, `unchanged`, `newCommits`, `squashed`, `sync`, `syncReason`

  - `outcome`: `"pushed"`
  - `published`: `string`
  - `source`: `string`
  - `previous` (optional): `string`
  - `via` (optional): `string`
  - `unchanged` (optional): `boolean`
  - `newCommits` (optional): `number`
  - `squashed` (optional): `"signed" | "unsigned"`
  - `sync`: [`SyncState`](type-syncstate.md)
  - `syncReason` (optional): `string`

**Example**

```ts
import { fail, jr2Setup, push, type PushOutput, workspace, type Workspaced } from "@jr2/orchestrator";

type Input = Workspaced<{ sha: string }, "app">;

// A push that did not happen (`rejected` or `diverged`) is a different problem from one jr2
// cannot see (`unknown`).
const notPushed = (output: PushOutput): boolean =>
  output.outcome === "rejected" || output.outcome === "diverged";

const body = jr2Setup({
  types: {} as { context: Input; input: Input },
  actors: { publish: push({ pin: true }), fail: fail() },
}).createMachine({
  id: "release",
  context: ({ input }) => input,
  on: { "workspace.lost": ".lost" },
  initial: "publishing",
  states: {
    publishing: {
      invoke: {
        src: "publish",
        input: ({ context }) => ({ sha: context.sha }),
        onDone: [
          { guard: ({ event }) => event.output.outcome === "pushed", target: "published" },
          { guard: ({ event }) => notPushed(event.output), target: "notPushed" },
          { target: "unknown" },
        ],
      },
    },
    published: { type: "final" },
    // Not final states: each one fails the run, with its own reason.
    notPushed: { invoke: { src: "fail", input: { reason: "the remote refused the push" } } },
    unknown: { invoke: { src: "fail", input: { reason: "jr2 cannot tell if the push happened" } } },
    lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
  },
});

export const machine = workspace(body, {
  repos: { app: { url: "https://github.com/you/app.git" } },
  spec: () => ({ branch: "release" }),
});
```

**See also**

- [Push and sign a branch](../../guides/push-and-sign-a-branch.md)
- [Effects](../../concepts/effects.md)
- [ADR-0067: An Effect is an actor slot, and runs at most once unless marked idempotent](../decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)
