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.
feature/my-feature ──b2b──► dev ──b2b──► main ──b2t──► v1.2.3
Each stage follows the same two-step plan / apply pattern:
- Plan — collect commits, generate a description/title/notes, write a
YAML file to
.shipit/plans/<hash>.yml. Nothing is created on the platform yet. - 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.
shipit init --platform-token <TOKEN> --platform-domain github.comThis writes shipit.toml and creates .shipit/plans/.
shipit b2b plan feature/my-feature devShipit 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):
shipit b2b plan feature/my-feature dev --conventional-commitsThe description will be grouped into sections (## Features, ## Bug Fixes,
## Infrastructure, etc.).
Agent-provided title and description (skip commit collection entirely):
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.
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:
PLAN=$(shipit b2b plan feature/my-feature dev --yaml -y)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
shipit b2b apply "$PLAN_FILE"Same commands, different branches:
# 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.
shipit b2t plan mainShipit finds the most recent tag reachable from main, collects commits since
that tag, and suggests the next semantic version.
Conventional-commit structured notes:
shipit b2t plan main --conventional-commits -yAgent-provided tag name and notes:
shipit b2t plan main v1.2.3 \
--description "$(cat <<'EOF'
## What's Changed
- New payment integration (#42)
- Fixed session timeout bug (#38)
EOF
)"shipit b2t apply <plan-filename>.ymlCreates an annotated local tag and pushes refs/tags/<name> to the remote.
Capturing the tag plan for agent use:
PLAN=$(shipit b2t plan main --conventional-commits -y --yaml)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
shipit b2t apply "$PLAN_FILE"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 callapplyautonomously.
The most powerful use of shipit for an agent is:
- Run
b2b planorb2t planwith--yamlto collect commits, write the plan file, and receive the plan on stdout. - Use
yqto extract theplan_filename (for theapplystep) and the boundary commit SHAs (forgit diff). - Run
git diff <first-sha>^..<last-sha>to get the full diff for the range. - Summarise the diff and rerun
planwith--descriptionand/or--titleto overwrite the auto-generated content with a human-quality summary. - Run
applyon the enriched plan.
# 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-dirtyCommits 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:
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 | Short | Description |
|---|---|---|
--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) |
| Flag | Description |
|---|---|
--allow-dirty |
Continue even if the working directory has uncommitted changes |
--remote <name> |
Git remote name (default: origin) |
--dir <path> |
Repository root (default: cwd) |
| Argument | Description |
|---|---|
[tag] |
Tag name to create (default: next semantic version derived from commits) |
| Flag | Short | Description |
|---|---|---|
--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) |
| Flag | Description |
|---|---|
--allow-dirty |
Continue even if the working directory has uncommitted changes |
--remote <name> |
Git remote name (default: origin) |
--dir <path> |
Repository root (default: cwd) |
Files written to .shipit/plans/ look like this:
# 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:
plan_file: 3f9a1c2e4d7b0e5f.ymlUse this field to drive the apply step without filesystem globbing.
# ── Feature → Dev ──────────────────────────────────────────────────────────
PLAN=$(shipit b2b plan feature/payments dev --conventional-commits -y --yaml --allow-dirty)
shipit b2b apply "$(echo "$PLAN" | yq '.plan_file')" --allow-dirty
# ── Dev → Main ─────────────────────────────────────────────────────────────
PLAN=$(shipit b2b plan dev main --conventional-commits -y --yaml --allow-dirty)
shipit b2b apply "$(echo "$PLAN" | yq '.plan_file')" --allow-dirty
# ── Main → Tag ─────────────────────────────────────────────────────────────
PLAN=$(shipit b2t plan main --conventional-commits -y --yaml --allow-dirty)
shipit b2t apply "$(echo "$PLAN" | yq '.plan_file')" --allow-dirty