shipit 1.3.1

Shipit is an open source command line interface for managing merge requests, changelogs, tags, and releases using a plan and apply interface. Built with coding agent integration in mind.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
# 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`

| Flag | Description |
|---|---|
| `--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:**

| Platform | Variables checked (in order) |
|---|---|
| GitHub | `GITHUB_TOKEN`, `GH_TOKEN` |
| GitLab | `GITLAB_TOKEN`, `GITLAB_PRIVATE_TOKEN` |

### `shipit b2b plan <source> <target>`

| 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) |

### `shipit b2b apply <plan-file>`

| 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) |

### `shipit b2t plan <branch> [tag]`

| 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) |

### `shipit b2t apply <plan-file>`

| 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) |

---

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

---

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