Skip to content

Cross-repo changes

Some backlog items need a change in more than one repository — an image-tag bump, a Helm value, an ArgoCD manifest, or a Terraform file in a sibling config repo. Echo supports this through supporting repos, and it treats any cross-repo change as an automatic hold for a human merge.

A project’s supportingRepos config lists the repos a worker is allowed to touch. Each entry names the repo slug and, optionally, a local clone to prefer:

"supportingRepos": [
{
"name": "infra",
"repo": "your-org/your-infra-config",
"localPath": "/home/you/projects/your-infra-config"
}
]

The list defaults to [] — most projects never touch a supporting repo, and the worker prompt’s cross-repo section is a no-op for them. The rendered list is injected into the worker prompt so the worker knows exactly which repos are in bounds.

When Step 5 execution demonstrably requires editing a declared repo, the worker runs checkout_supporting_repo.sh, which lazily creates an isolated worktree:

Terminal window
DEST="$(bash .../echo-core/scripts/checkout_supporting_repo.sh \
--project "$ROOT" --name infra --issue 42)"
cd "$DEST"
  1. It resolves the source: explicit localPath first, then the sibling-directory convention (../<repo-basename> next to the project root), then a remote gh repo clone fallback.

  2. It creates a worktree at <worktree_base>/issue-<N>/<name> on branch <branchPrefix><N> (with an optional -<slug> suffix). The checkout is idempotent — a healthy existing worktree on the expected branch is handed straight back; an unhealthy one (wrong branch, not a git dir) is pruned and re-created.

  3. It prints the worktree path on stdout. The worker edits, commits, and pushes there with the same discipline as its main worktree — no --no-verify, no amend.

The worker then opens the sibling PR with Refs <board-repo>#<N>never Closes. The Closes keyword only auto-closes within the issue’s own repo; in a sibling repo it silently does nothing, leaving the issue open forever with no error anywhere. The main-repo PR body lists every sibling PR under a ## Cross-repo PRs (merge first) heading so a human sees the merge order.

On every successful checkout, checkout_supporting_repo.sh drops an issue-scoped sentinel file:

<state_dir>/cross-repo-touched.<issue>

This is the mechanism that forces a human merge. It’s written on the idempotent short-circuit path and every fresh-checkout success path, and its write is strictly best-effort (|| true) — a sentinel-write failure never fails the checkout.

The sentinel is the first deterministic gate in gated_merge.sh. Before any other gate, it checks:

if [[ -f "$STATE_DIR/cross-repo-touched.$num" ]]; then
hold "cross-repo change — human merge required (PR #$pr + sibling repo PR(s))"
fi

So a cross-repo item is never auto-merged, regardless of merge.mode or any review verdict. The worker’s job is unchanged from any other run: open every PR (main + sibling), list the siblings under ## Cross-repo PRs (merge first), label the main PR echo:merge-ready, and stop. It merges nothing — it has no merge command. A human then merges the sibling PR(s) first, in the listed order, then the main PR.