# Shipit — AI Agent Guide
This document describes how a coding agent can use shipit to drive a
**git flow lite** release process: feature branches land in `dev`, `dev`
promotes to `main`, and `main` is tagged for release.
---
## Workflow Overview
```
feature/my-feature ──b2b──► dev ──b2b──► main ──b2t──► v1.2.3
```
Each stage follows the same two-step **plan / apply** pattern:
1. **Plan** — collect commits, generate a description/title/notes, write a
YAML file to `.shipit/plans/<hash>.yml`. Nothing is created on the platform yet.
2. **Apply** — read the plan file and execute: open a PR/MR, or create and push
the annotated tag.
The plan file is the agent's opportunity to review, enrich, or rewrite any field
before anything is published.
---
## Setup
```bash
shipit init
```
This writes `shipit.toml` and creates `.shipit/plans/`.
Both `--platform-domain` and `--platform-token` are optional — shipit discovers
sensible defaults automatically:
- **Domain** — inferred from the `origin` remote URL (or the remote named by
`--remote`). SSH (`git@github.com:…`) and HTTPS (`https://github.com/…`)
formats are both recognised.
- **Token** — looked up from the environment based on the resolved domain:
- GitHub: `GITHUB_TOKEN`, then `GH_TOKEN`
- GitLab: `GITLAB_TOKEN`, then `GITLAB_PRIVATE_TOKEN`
When running `shipit init` non-interactively (e.g. in CI or as part of an
agent workflow), pass the flags explicitly to skip all prompts:
```bash
shipit init --platform-domain github.com --platform-token "$GITHUB_TOKEN"
```
---
## Stage 1 — Feature Branch → Dev
### 1a. Generate a plan
```bash
shipit b2b plan feature/my-feature dev
```
Shipit collects commits on `feature/my-feature` that are not yet on `dev`,
enriches them with PR/MR titles from the platform API, and writes a plan file.
**Conventional-commit structured description** (recommended when commits follow
the conventional-commit convention):
```bash
shipit b2b plan feature/my-feature dev --conventional-commits
```
The description will be grouped into sections (`## Features`, `## Bug Fixes`,
`## Infrastructure`, etc.).
**Agent-provided title and description** (skip commit collection entirely):
```bash
shipit b2b plan feature/my-feature dev \
--title "feat: add payment integration" \
--description "$(cat <<'EOF'
## Summary
- Adds Stripe checkout flow
- Introduces `PaymentService` with retry logic
- Updates API contract in `openapi.yml`
EOF
)"
```
When both `--title` and `--description` are supplied, no commits are collected
and the plan is written immediately.
### 1b. Apply the plan
```bash
shipit b2b apply <plan-filename>.yml
```
`<plan-filename>` is the filename (not a full path) of the file written to
`.shipit/plans/` in the previous step. Shipit opens the pull/merge request
and prints the URL.
**Capturing the plan for agent use** — pass `--yaml` to receive the full plan
on stdout. The output includes a `plan_file` field with the filename ready for
`apply`, and the `commits` list for extracting commit SHAs:
```bash
PLAN=$(shipit b2b plan feature/my-feature dev --yaml -y)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
shipit b2b apply "$PLAN_FILE"
```
---
## Stage 2 — Dev → Main
Same commands, different branches:
```bash
# Auto-generate from commits (conventional-commit format)
PLAN=$(shipit b2b plan dev main --conventional-commits -y --yaml)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
# Apply
shipit b2b apply "$PLAN_FILE"
```
The `-y` flag skips the interactive title prompt and accepts the suggested
`"Release Candidate vX.Y.Z"` title derived from the commit history.
---
## Stage 3 — Main → Tag
### 3a. Generate a tag plan
```bash
shipit b2t plan main
```
Shipit finds the most recent tag reachable from `main`, collects commits since
that tag, and suggests the next semantic version.
**Conventional-commit structured notes:**
```bash
shipit b2t plan main --conventional-commits -y
```
**Agent-provided tag name and notes:**
```bash
shipit b2t plan main v1.2.3 \
--description "$(cat <<'EOF'
## What's Changed
- New payment integration (#42)
- Fixed session timeout bug (#38)
EOF
)"
```
### 3b. Apply the tag plan
```bash
shipit b2t apply <plan-filename>.yml
```
Creates an annotated local tag and pushes `refs/tags/<name>` to the remote.
**Capturing the tag plan for agent use:**
```bash
PLAN=$(shipit b2t plan main --conventional-commits -y --yaml)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
shipit b2t apply "$PLAN_FILE"
```
---
## Agent-Enriched Plans (Recommended Pattern)
> **Important for AI agents:** Always present the final plan to the user and
> wait for explicit approval before running `apply`. Opening a pull/merge
> request or pushing a tag is irreversible — the plan step exists precisely
> to give the user a review checkpoint. Never call `apply` autonomously.
The most powerful use of shipit for an agent is:
1. Run `b2b plan` or `b2t plan` with `--yaml` to collect commits, write the
plan file, and receive the plan on stdout.
2. Use `yq` to extract the `plan_file` name (for the `apply` step) and the
boundary commit SHAs (for `git diff`).
3. Run `git diff <first-sha>^..<last-sha>` to get the full diff for the range.
4. Summarise the diff and rerun `plan` with `--description` and/or `--title` to
overwrite the auto-generated content with a human-quality summary.
5. Run `apply` on the enriched plan.
### Example: agent-enriched b2b plan
```bash
# Step 1 — write the initial plan and capture the YAML output
PLAN=$(shipit b2b plan feature/payments dev --conventional-commits -y --yaml --allow-dirty)
# Step 2 — extract the plan filename and boundary commit SHAs with yq
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
LAST_SHA=$(echo "$PLAN" | yq '.commits[0]' | awk '{print $NF}')
FIRST_SHA=$(echo "$PLAN" | yq '.commits[-1]' | awk '{print $NF}')
# Step 3 — get the diff
DIFF=$(git diff "${FIRST_SHA}^".."${LAST_SHA}")
# Step 4 — ask the agent to summarise the diff, then rerun plan with the result
SUMMARY="<agent-generated summary goes here>"
PLAN=$(shipit b2b plan feature/payments dev \
--title "feat(payments): Stripe checkout integration" \
--description "$SUMMARY" \
--yaml --yes --allow-dirty)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
# Step 5 — present the plan to the user for confirmation before applying
echo "$PLAN"
# Step 6 — apply only after the user approves
shipit b2b apply "$PLAN_FILE" --allow-dirty
```
### Commit ordering in the `commits` list
Commits are stored newest-first under the `commits:` key. Each entry is the
string `"<message> <sha>"` — the SHA is always the last whitespace-separated
token. Use index `0` for the newest commit and `-1` for the oldest:
```bash
PLAN=$(shipit b2b plan feature/payments dev --yaml -y)
LAST_SHA=$(echo "$PLAN" | yq '.commits[0]' | awk '{print $NF}') # newest
FIRST_SHA=$(echo "$PLAN" | yq '.commits[-1]' | awk '{print $NF}') # oldest
git diff "${FIRST_SHA}^".."${LAST_SHA}"
```
The diff can then be passed to the agent's language model to generate a
structured description before calling `plan` again with `--description`.
---
## Flag Reference
### `shipit init`
| `--platform-domain <domain>` | Platform domain (default: inferred from remote URL) |
| `--platform-token <token>` | Platform personal access token (default: inferred from env — see below) |
| `--remote <name>` | Git remote to infer the domain from (default: `origin`) |
| `--dir <path>` | Directory to write config to (default: cwd) |
**Token environment variable lookup order:**
| GitHub | `GITHUB_TOKEN`, `GH_TOKEN` |
| GitLab | `GITLAB_TOKEN`, `GITLAB_PRIVATE_TOKEN` |
### `shipit b2b plan <source> <target>`
| `--conventional-commits` | `-c` | Group description by commit type |
| `--title <text>` | | Override the suggested PR/MR title |
| `--description <text>` | | Override the auto-generated description |
| `--only-merges` | | Restrict commit collection to merge commits |
| `--no-sign` | | Omit the "generated by Shipit" footer |
| `--yes` | `-y` | Accept all prompts non-interactively |
| `--yaml` | | Emit the plan as YAML to stdout (includes `plan_file` field) |
| `--allow-dirty` | | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | | Git remote name (default: `origin`) |
| `--dir <path>` | | Repository root (default: cwd) |
### `shipit b2b apply <plan-file>`
| `--allow-dirty` | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | Git remote name (default: `origin`) |
| `--dir <path>` | Repository root (default: cwd) |
### `shipit b2t plan <branch> [tag]`
| `[tag]` | Tag name to create (default: next semantic version derived from commits) |
| `--conventional-commits` | `-c` | Group notes by commit type |
| `--description <text>` | | Override the auto-generated tag notes |
| `--latest-tag <name>` | | Compare against a specific tag instead of auto-detecting |
| `--only-merges` | | Restrict commit collection to merge commits |
| `--no-sign` | | Omit the "generated by Shipit" footer |
| `--yes` | `-y` | Accept all prompts non-interactively |
| `--yaml` | | Emit the plan as YAML to stdout (includes `plan_file` field) |
| `--allow-dirty` | | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | | Git remote name (default: `origin`) |
| `--dir <path>` | | Repository root (default: cwd) |
### `shipit b2t apply <plan-file>`
| `--allow-dirty` | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | Git remote name (default: `origin`) |
| `--dir <path>` | Repository root (default: cwd) |
---
## Plan File Format
Files written to `.shipit/plans/` look like this:
```yaml
# Shipit Plan - Generated by shipit v0.5.0 on 2024-06-01T12:00:00Z
shipit_version: 0.5.0
generated_at: "2024-06-01T12:00:00Z"
source: feature/payments
target: dev
title:
value: "Release Candidate v1.2.0"
generated_by: default # "user" | "default" | "conventional-commits" | "raw"
description:
value: |
## Features
- feat: add Stripe checkout flow abc123
generated_by: conventional-commits
commits:
- "feat: add Stripe checkout flow abc123 a1b2c3d4"
- "fix: handle webhook timeout def456 e5f6a7b8"
```
The `generated_by` field records what produced each value so downstream
tooling (and the agent) can decide whether to trust it or regenerate it.
When `--yaml` is passed, the stdout output adds a `plan_file` field not
present in the written file:
```yaml
plan_file: 3f9a1c2e4d7b0e5f.yml
```
Use this field to drive the `apply` step without filesystem globbing.
---
## Complete Git Flow Lite Example
```bash
# ── Feature → Dev ──────────────────────────────────────────────────────────
PLAN=$(shipit b2b plan feature/payments dev --conventional-commits -y --yaml --allow-dirty)
# ── Dev → Main ─────────────────────────────────────────────────────────────
PLAN=$(shipit b2b plan dev main --conventional-commits -y --yaml --allow-dirty)
# ── Main → Tag ─────────────────────────────────────────────────────────────
PLAN=$(shipit b2t plan main --conventional-commits -y --yaml --allow-dirty)
---
## Multi-Project Release Workflow
This section describes how an agent can coordinate releases across **multiple
repositories** in a defined order. Each project has its own environment
pipeline (the ordered sequence of branch-to-branch promotions and/or
branch-to-tag operations). A shared config file persists these settings
across workflow runs.
---
### Config File
The multi-project config lives at `.shipit/multi-release.yml` relative to the
directory where the agent is invoked (typically a workspace or monorepo root,
but it can also be any convenient location).
**Format:**
```yaml
# .shipit/multi-release.yml
# Projects are released in the order they appear in this list.
projects:
- name: api-service
dir: /absolute/path/to/api-service # directory that contains shipit.toml
pipeline:
- type: b2b # branch-to-branch PR/MR
source: dev
target: qa
- type: b2b
source: qa
target: main
- type: b2t # branch-to-tag
source: main
- name: frontend
dir: /absolute/path/to/frontend
pipeline:
- type: b2b
source: dev
target: staging
- type: b2b
source: staging
target: main
- type: b2t
source: main
- name: infra
dir: /absolute/path/to/infra
pipeline:
- type: b2b
source: dev
target: main
- type: b2t
source: main
```
**Field reference:**
| Field | Description |
|---|---|
| `name` | Human-readable project label used in agent prompts |
| `dir` | Absolute path to the project root (must contain `shipit.toml`) |
| `pipeline` | Ordered list of release steps for this project |
| `pipeline[].type` | `b2b` (branch-to-branch) or `b2t` (branch-to-tag) |
| `pipeline[].source` | Source branch |
| `pipeline[].target` | Target branch (`b2b` only) |
---
### Agent Decision Tree
Every time the multi-project workflow is triggered, the agent follows this
decision tree before executing any release steps.
```
START
│
▼
Does .shipit/multi-release.yml exist?
│
├─ NO ──► [First-Run Setup] ──► write config ──► continue
│
└─ YES
│
▼
Ask the user:
"I found the following release config:
1. api-service (dev → qa → main → tag)
2. frontend (dev → staging → main → tag)
3. infra (dev → main → tag)
Do you want to run the full release for all projects, or is this
an atypical run (e.g. a subset of projects, or fewer environment
steps)?"
│
├─ FULL ──► use config as-is ──► execute
│
└─ ATYPICAL
│
▼
Collect overrides from user:
- Which projects? (subset / reordering)
- For each project, which pipeline steps? (subset / reordering)
Do NOT write changes back to config.
│
▼
Execute with overrides only
```
---
### First-Run Setup
When `.shipit/multi-release.yml` does not exist, the agent must collect
configuration from the user interactively before proceeding.
**Prompts to ask (in order):**
1. "How many projects do you want to include in the multi-project release
workflow? Please list them in release order."
2. For each project in order:
- "What is the name of this project?"
- "What is the absolute path to this project's directory?"
- "What is the environment pipeline for this project?
List the steps in order, e.g.:
`dev → qa` (b2b), `qa → main` (b2b), `main → tag` (b2t)"
3. Show the agent's interpretation of the collected config in YAML form and
ask the user to confirm before writing the file.
Once confirmed, write `.shipit/multi-release.yml` and proceed with the
release using the new config.
---
### Executing the Workflow
**Before iterating over pipeline steps**, verify that every project in the
effective run has been initialised with `shipit init` by checking for a
`shipit.toml` file in its `dir`:
```bash
# For each project
test -f <project-dir>/shipit.toml || echo "MISSING"
```
If any project is missing `shipit.toml`, run `shipit init` for it before
continuing:
```bash
shipit init --dir <project-dir>
```
Do not proceed with any pipeline steps until all projects report a valid
`shipit.toml`.
---
For each project (in config order, or user-specified order for atypical runs),
for each pipeline step (in config order, or user-specified subset):
1. `cd` into the project's `dir`.
2. Check out the source branch for the current step:
```bash
git -C <project-dir> checkout <source>
```
3. If the step is `b2b`:
```bash
PLAN=$(shipit b2b plan <source> <target> \
--conventional-commits -y --yaml --allow-dirty \
--dir <project-dir>)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
```
If the step is `b2t`:
```bash
PLAN=$(shipit b2t plan <source> \
--conventional-commits -y --yaml --allow-dirty \
--dir <project-dir>)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
```
4. Record the plan outcome for the summary table (see step 9). A step has:
- **No changes** — the plan's `commits` list is empty.
- **Success** — the plan was generated without error and has commits.
- **Failed** — the plan command exited with an error.
5. Present the plan to the user and **wait for explicit approval** before
calling `apply`. This is mandatory — see the warning in the
[Agent-Enriched Plans](#agent-enriched-plans-recommended-pattern) section.
6. On approval:
```bash
shipit b2b apply "$PLAN_FILE" --allow-dirty --dir <project-dir>
shipit b2t apply "$PLAN_FILE" --allow-dirty --dir <project-dir>
```
7. After **all** pipeline steps across **all** projects have been planned
(regardless of how many were applied), output a Markdown summary table:
| Project | Step | Plan ID | Result | Title / Tag |
|---|---|---|---|---|
| api-service | dev → qa | `3f9a1c2e4d7b0e5f.yml` | ✓ success | feat: add payment integration |
| api-service | qa → main | `a1b2c3d4e5f60718.yml` | ✓ success | Release Candidate v1.4.0 |
| api-service | main → tag | `9e8d7c6b5a4f3e2d.yml` | ✓ success | v1.4.0 |
| frontend | dev → staging | — | — no changes | — |
| infra | dev → main | — | ✗ failed | — |
**Result values:**
- `✓ success` — plan generated with commits present
- `— no changes` — plan's `commits` list was empty; nothing to release
- `✗ failed` — plan command exited with an error (include the error message in a note below the table)
The **Plan ID** column shows the `plan_file` value from the plan YAML (e.g.
`3f9a1c2e4d7b0e5f.yml`). Use `—` when no plan was generated (no changes or
failed).
The **Title / Tag** column shows the `title.value` from the plan YAML for
`b2b` steps, or the tag name for `b2t` steps. Use `—` when there is
nothing to show (no changes or failed).
**Do not proceed to the next pipeline step or the next project until the
current step succeeds and the user approves.**
---
### Handling Atypical Runs
When the user indicates a non-standard run (a subset of projects, fewer
environment steps, a different order), the agent must:
1. Collect the exact scope from the user — confirm each override explicitly.
2. Echo back the effective plan ("I will release `api-service` dev→qa only,
then `infra` dev→main→tag") and wait for confirmation.
3. Execute using the collected overrides.
4. **Never write overrides back to `.shipit/multi-release.yml`.** The
persisted config always reflects the canonical full-release workflow.
---
### Example: Full Release Session
```
Agent: I found the following release config:
1. api-service — dev → qa (b2b) → main (b2b) → tag (b2t)
2. frontend — dev → staging (b2b) → main (b2b) → tag (b2t)
3. infra — dev → main (b2b) → tag (b2t)
Is this a full release across all projects and environments, or do
you want to adjust the scope?
User: Full release.
Agent: Starting release for project 1/3: api-service
Step 1/3: dev → qa
[generates plan, shows summary]
Proceed with applying this plan? (yes/no)
User: yes
Agent: ✓ PR opened: https://github.com/org/api-service/pull/42
Step 2/3: qa → main
[generates plan, shows summary]
Proceed with applying this plan? (yes/no)
User: yes
Agent: ✓ PR opened: https://github.com/org/api-service/pull/43
Step 3/3: main → tag
[generates tag plan, shows summary]
Proceed with applying this plan? (yes/no)
User: yes
Agent: ✓ Tag v1.4.0 created and pushed.
Moving to project 2/3: frontend
...
```
---
### Example: Atypical Run Session
```
Agent: I found the following release config:
1. api-service — dev → qa (b2b) → main (b2b) → tag (b2t)
2. frontend — dev → staging (b2b) → main (b2b) → tag (b2t)
3. infra — dev → main (b2b) → tag (b2t)
Is this a full release across all projects and environments, or do
you want to adjust the scope?
User: Just release api-service and infra, skip tagging infra.
Agent: Got it — atypical run (will not update your saved config).
Here is the effective plan:
1. api-service — dev → qa (b2b) → main (b2b) → tag (b2t)
2. infra — dev → main (b2b) [tag step skipped]
Confirm? (yes/no)
User: yes
Agent: Starting release for project 1/2: api-service
...
```