Skip to main content

multirepo-ci-cd

Diagnose a CI failure or roll a workflow out across many repos. Use when debugging a red check in a multi-repo org, promoting a shared reusable-*.yml, or re-triggering a PR after a @main fix.

Informations de source

Dépôt
laurigates/claude-plugins
Dernière activité de la source
12 septembre 2026 à 18:54
Langue détectée de SKILL.md
anglais
Étoiles
58
Forks
6

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
multirepo-ci-cd
description
Diagnose a CI failure or roll a workflow out across many repos. Use when debugging a red check in a multi-repo org, promoting a shared reusable-*.yml, or re-triggering a PR after a @main fix.
allowed-tools
Bash, Read, Grep, Glob, TodoWrite
created
2026-08-06T00:00:00.000Z
modified
2026-09-12T00:00:00.000Z
reviewed
2026-09-12T00:00:00.000Z
# Multi-Repo CI/CD Discipline Org-agnostic lessons for working across a portfolio of repos that share reusable workflows. The org-specific workflow catalogs live in the child rules (`*/.claude/rules/ci-cd-workflows.md`). ## Diagnosing a CI Failure — Fetch First Before investigating why a workflow failed, `git fetch` and compare local `HEAD` against `origin/main`. In a multi-repo portfolio, local checkouts go stale, and **the failing workflow file (or its config — `release-please-config.json`, the manifest, a new `.yml`) may have been added in commits you haven't pulled.** It won't exist in your local tree, so you'll search for a "missing" file that CI is actively running. One `git fetch` + `git log --oneline HEAD..origin/main` shows the gap; `git pull --ff-only` syncs it. Diagnose against what CI actually ran, not a stale snapshot. ## Rolling a Workflow Out to a Repo Class When promoting a self-contained workflow into a shared `reusable-*.yml` and adding a thin caller to every repo of a class, three things bite if missed: 1. **IaC feature flags are the source of truth for the repo set.** Don't hand-list repos — derive the class from the gitops/Terraform definitions (e.g. `release_please = true` entries in `repositories.tf` *are* the membership list). Extract them rather than guessing which repos qualify: ``` awk '/^ "[^"]+" = \{/ { match($0, /"[^"]+"/); name=substr($0, RSTART+1, RLENGTH-2) } /release_please[ ]*=[ ]*true/ { print name }' repositories.tf ``` 2. **Land the reusable workflow on the `.github` repo's `main` first.** Callers pin `uses: <owner>/.github/...@main`, which resolves at dispatch time — a caller merged or dispatched before the reusable workflow exists on `main` fails. Merge the `.github` PR before opening (or at least before merging) the callers. `workflow_dispatch`-only callers are harmless until triggered, so they can be opened in parallel, but the `.github` change is the prerequisite. 3. **Bulk-create callers via the GitHub API, not local clones.** The caller body is identical across repos, and some repos may not be checked out locally. Creating the branch + file + PR through the API is uniform and immune to local working-tree state (stale checkouts, wrong branch, uncommitted changes): ``` sha=$(gh api repos/<owner>/$r/git/ref/heads/main --jq .object.sha) gh api -X POST repos/<owner>/$r/git/refs -f ref=refs/heads/$BRANCH -f sha=$sha gh api -X PUT repos/<owner>/$r/contents/$PATH -f message=$MSG -f content=$B64 -f branch=$BRANCH gh pr create -R <owner>/$r --base main --head $BRANCH -a laurigates --title "$MSG" --body "$BODY" ``` Loop sequentially (not a parallel batch — siblings cancel on the first non-zero exit), and skip a repo if the file already exists on `main`. Title the caller PR `ci:` so release-please does not cut a version bump for a CI-only change. When the running gh user is the repo owner, omit `--add-reviewer` (self-review is rejected 422). ## Bulk-*Read* Through the API Too — a Stale Checkout Produces Wrong Findings The API-not-clones rule above is usually framed as a *write* discipline. It is equally a **read** discipline: a portfolio-wide `rg`/`grep` over local checkouts answers a question about *whatever each clone last fetched*, not about the repos. In a side-by-side layout where most clones are touched rarely, that answer is confidently wrong — and wrong in the direction of "this pattern isn't used anywhere," because a checkout predating an adoption simply lacks it. > Evidence (2026-07, ubuntu-slim runner sweep): a local `grep -rl` concluded > **0 repos** called `reusable-release-please.yml` and all 29 inlined > `googleapis/release-please-action` directly. That statement was made to the > user as fact. `pal-mcp-server` had migrated to the reusable on the remote > *weeks earlier*; the local clone was stale. Nothing about the grep output > looked suspicious. - **Read each file from origin in the same pass that patches it**: `gh api repos/<owner>/<repo>/contents/<path> --jq .content | base64 -d`. One fetch per repo is cheap and makes the finding and the edit agree. - **Use the local checkouts only to *generate candidates*, never to state conclusions.** Enumerate broadly locally (it's fast), then confirm every claim against the remote before reporting or acting on it. - **Make the patch function assert its precondition and raise.** The stale `pal-mcp-server` clone was caught only because the sweep's patcher demanded a `runs-on:` inside the named job and raised *"no runs-on found inside release-please job"* rather than falling through to a no-op or a mangled file. A silent `if found: patch` would have skipped it invisibly — and the wrong finding would have stood. Same instinct as `git-hazards.md`: a green run is not proof the content is right. - Cross-check the repo set itself against the remote as well (`gh repo list <owner> --json name,isArchived,isFork`) — a local directory can be a fork, an archived repo, or have no remote at all. ## Landing a `@main` Reusable-Workflow Fix on an Existing PR — Re-Trigger, Don't Re-Run After you merge a fix to a `reusable-*.yml` on the `.github` repo's `main`, the open PRs whose callers pin `@main` do **not** pick it up by re-running the failed check. **A GitHub re-run replays the original run against the workflow resolution it was first dispatched with** — including the exact commit the `@main` reusable workflow resolved to at that time. So `gh run rerun` / "Re-run failed jobs" / the `rerun-failed-jobs` API all replay the *stale* reusable workflow and fail identically, even though `main` is now fixed. The tell: you read the re-run's log and it shows the *old* behaviour (the command you just replaced still running), not your fix. **Fix: force a fresh `pull_request` event**, which re-resolves `@main` at dispatch time and picks up the merged fix. Cheapest first: ``` gh pr update-branch <n> -R <owner>/<repo> # fires `synchronize`; no-op-safe if already current ``` If the branch is already current (update-branch reports "already up to date"), close-then-reopen fires `reopened` instead: ``` gh pr close <n> -R <owner>/<repo> && gh pr reopen <n> -R <owner>/<repo> ``` For a **release-please** PR, prefer `update-branch` — closing the release PR is safe (release-please acts on push-to-`main`/schedule, not PR-close events) but reopening is noisier than a branch update. Avoid pushing to a release-please branch by hand. There is a second replay trap with the same shape: a `pull_request` re-run also replays the original event payload, so its `sender` is whoever opened the PR — a **bot** for Renovate/release-please PRs. Actions with a human-actor guard (e.g. `anthropics/claude-code-action` without `allowed_bots`) therefore fail with "Workflow initiated by non-human actor" on re-run **regardless of who clicked re-run**. A fresh event doesn't change the sender (still the bot), so the real fix is `allowed_bots` on the action, not re-triggering — but the same "re-run replays the original payload" mechanic is why clicking re-run yourself doesn't help. Both traps are the same root cause: a re-run is a replay, not a new dispatch.
Voir sur GitHub