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.

Ir a la instalación

Datos de origen

Repositorio
laurigates/claude-plugins
Última actividad en el origen
12 de septiembre de 2026 a las 18:54
Idioma detectado de SKILL.md
inglés
Estrellas
58
Forks
6

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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.
Ver en GitHub