--- name: git-workflows description: Reusable git delivery workflows derived from local slash commands (commit, push, PR, release notes, GitHub Actions failure triage with worktree-based fixes, and isolating your own changes from a shared dirty workspace). --- # Git Workflows (from local commands) This skill turns the repo's former local command templates into reusable git delivery workflows. ## When to use this skill Use this skill when the user asks to: - run a commit workflow (conventional-changelog style) - push changes with safe branching and open a PR - generate release notes from git history / GitHub context - publish a version from `main` (including bilingual README Recent updates on minor bumps) - analyze the latest failed GitHub Actions workflow, attempt a fix in an isolated worktree, and submit a PR - move your own uncommitted changes onto a clean branch when the current workspace also holds someone else's edits - check IDE icon configuration consistency across document components and source files ## Source of truth All detailed steps and constraints live in: - `references/source-commands.md` This skill describes how to apply them consistently across agents/editors without duplicating implementation details. ## Execution rules 1. Read the requested command template from `references/source-commands.md`. 2. Follow it as workflow steps, not as a loose summary. 3. Keep actions safe by default: - avoid destructive git operations unless explicitly requested - avoid committing secrets - keep changes minimal and localized 4. If a workflow implies external side effects (push/PR/release), request explicit confirmation right before performing the side effect. ## Repo-specific gotchas (CloudBase-MCP) - **Sync dependencies before building on `main`.** A stale `node_modules` shows up as a type error in code that is actually correct (seen: `@cloudbase/manager-node` 5.8.6 installed vs 5.8.8 locked → `Property 'ExternalStorage' does not exist on type 'CreateEnvParams'`). Verify with `npm pack @` and read the `.d.ts` before touching code. - **`pnpm install` fails inside the sandbox** with `ERR_PNPM_CODEBUDDY_BROKER_DENY` (symlink ENOENT), and the failed run deletes the package directory that was previously there. Re-run the same command with sandbox disabled before concluding anything about the build. - **`npm run build` succeeds even when the last step is blocked.** The build's `rm -rf dist/types` is refused by the safe-delete hook (325 files > threshold), so the log ends with a `SAFE_DELETE_BULK_CONFIRM_REQUIRED` line and `dist/types/` is left behind. Judge the build by whether webpack printed `ERROR in`, not by that trailing message. - **Release-note scope is `git log ..HEAD`**, including merge commits. A PR number lower than the current version does not mean it already shipped — PRs often merge after the tag that "should" have contained them. - **`CHANGELOG.md`'s `## Unreleased` is not rotated on release** in this repo; publish a GitHub Release and leave the changelog alone unless asked. - **Version bump surface**: `mcp/package.json` + `config/source/**` skill versions (`node scripts/sync-skill-versions.mjs --version X.Y.Z`) + `config/.claude/skills` mirror (`npm run sync:claude-skills-mirror`) + optional README bullets (patch: only for clearly user-visible capability, and both `README.md` and `README.zh-CN.md`, verified with `npm run check:readme-sync`). ## Redacting internal references from a pushed PR Public-repo PR metadata — the title, the body, each commit headline, each commit message — is as public as the diff, yet it never appears *in* the diff, so no file-layer guard covers it. `npm run check:internal-refs` enforces this in CI (see `internal-docs-guard.yml`). When a reference has already been pushed, fix it in place: 1. **Enumerate every source before editing anything.** `gh pr view --json commits` *truncates* headlines — read messages from git instead (`git log --format='%s%n%b' origin/main..`) and the body via `gh api repos///pulls/ --jq .body`. Body and commit messages are separate fixes; doing only one is the usual miss. 2. **Amend the message, then prove nothing else moved:** `git commit --amend -F -`, followed by `git diff --stat HEAD` — it must be empty. 3. **Force-push with `--force-with-lease`.** The repo rule says "no `--force`", but rewriting an already-pushed message has no alternative; the lease form still refuses to clobber a concurrent update. 4. **Rewrite the body** with `gh pr edit --body-file -`. 5. **Clear local-only commits** carrying the same problem: back up first (`git diff > /tmp/residual.patch`), then `git reset --hard origin/main`. The content is normally already in the PR it was split out of, so nothing is lost — verify each file is covered by a branch before resetting. A `--force-with-lease` push re-triggers CI, and this repo's `Publish MCP Package to pkg.pr.new` job runs live cloud integration tests that flake on timeouts (seen: cloud-function create/call at 60s). Read the failed test name before assuming the rewrite broke something — a message-only amend cannot. ## Recovering a stacked PR whose base was squash-merged When PR B is stacked on branch A (`base = A's branch`) and A is **squash**-merged into `main`, A's commits on `main` get **new shas**. A's branch is no longer an ancestor of `main`, but B's base pointer still points at it — so B turns `CONFLICTING` even though nothing in it changed. Any PR merged *into* A's branch in the meantime (a deeper stack) makes the collision certain. 1. **Pre-flight the conflict set before touching anything:** `git merge-tree --write-tree ` lists the conflicting paths directly. Cheap, and it tells you which files need a decision. 2. **Rebase onto `main`, dropping the already-merged commit:** `git rebase --onto origin/main `. Dropping it is the point — its content is on `main`, and replaying it would re-apply an already-merged change. Do **not** rebase onto A's stale branch. 3. **Resolve by file type** (these three recur): - `CHANGELOG.md` — keep **both** sides; bullet order is irrelevant. Re-align blank-line spacing if the merge changed it. - `package.json` — take the **union**; the added `scripts` entries come from different PRs and almost never truly conflict. - `config/source/editor-config/compat-baseline.json` — take `main`'s side (`git checkout --ours` during rebase), then **recompute** after the rebase finishes with `node scripts/update-compat-baseline.mjs`. Sanity check: the recomputed diff should match the original commit's line count (a version bump touching 3 skills produced 23±, i.e. the same 46 lines). 4. **Fold the recomputed baseline into the commit that caused it** — `git commit --fixup=` then `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash `. It belongs to the version bump, not a standalone commit. `git commit --fixup` sometimes prints `nothing to commit` and still squashes correctly afterwards — judge by `git log`, not that message. 5. **Retarget the base and rewrite the description:** `gh pr edit --base main`; the PR now carries the whole stack, so its title and body must cover every commit in it, not just the top one. 6. **Verify on the rebased content**, not just by "no conflict markers": run the version check, the compat diff, and the unit tests. If the PR adds a CI gate, running that gate against `origin/main` on this branch is the strongest available check — it exercises the gate against real input. ### Trap: a `paths`-filtered workflow can silently not run at all After a force-push (`synchronize`) while the base was still the stale stacked branch, the workflows that carry a `paths:` filter (`compat-check.yml`, `check-prompts-sync.yml`) **did not trigger at all** — only the unfiltered `internal-docs-guard.yml` and the push-triggered `Sync to CNB` ran. `gh pr checks` does **not** list checks that never ran, so the PR looked clean while two checks had simply been skipped. - **Judge with the API, not the UI:** `gh api "repos///actions/runs?branch=" --jq '.workflow_runs[] | "\(.name) \(.event) \(.conclusion)"'`. A workflow missing from that list was skipped, not passed. - **Fix:** `gh pr close ` then `gh pr reopen `. The `reopened` event recomputes against the new base and triggers the filtered workflows normally. ### When the PR is not stacked and `main` simply moved A plain PR turns `CONFLICTING` the same way — usually on `config/source/editor-config/compat-baseline.json` alone, since every branch that regenerates it rewrites the same lines. `git merge origin/main` is fine here: the squash on merge erases the extra merge commit, and there is no already-merged commit to drop. Watch the direction — during a **merge** `main` is `--theirs` (`git checkout --theirs …`); during a **rebase** it is `--ours`. Then regenerate the whole chain on top of the merge rather than hand-resolving the generated side: 1. `node scripts/generate-prompts-data.mjs`, then `node scripts/generate-prompts.mjs` 2. `node scripts/sync-claude-skills-mirror.mjs` 3. `node scripts/build-compat-config.mjs` 4. `node scripts/update-compat-baseline.mjs` Merging `main` typically brings changes under `config/source/skills/**` as well, and those propagate into every IDE mirror and every generated surface — recomputing only the baseline leaves `compat-diff` or `prompts-sync` red. Close with `node scripts/diff-compat-config.mjs` (`Has blocking diff: NO`), `node scripts/check-prompts-sync.mjs`, and `node scripts/sync-claude-skills-mirror.mjs --check`. ## Isolating your own changes from a shared dirty workspace A workspace can hold several people's uncommitted work at once, sometimes on a branch whose PR already merged. Committing there means either shipping their work or fighting the index. Move your files onto a clean branch instead. 1. **Split by ownership before anything else.** `git status --porcelain` lists the whole workspace; `git diff --stat -- ` against `git diff --stat -- . ':(exclude)'` separates yours from theirs. Trace each of your files back to the edit that produced it — a workspace is not yours just because you were the last one in it. 2. **Snapshot your files as a patch, not a stash.** `git diff -- > /tmp/.patch`, then confirm the set with `grep -E '^diff --git' /tmp/.patch`. A patch travels between worktrees; a stash is per-worktree and easy to strand. 3. **Base the new branch on the fetched `origin/main`.** `git fetch origin` then `git worktree add -b .worktrees/ origin/main`. Never base it on the current branch — if that branch's PR was squash-merged, its commits are already on `main` under new shas. 4. **Give the new worktree `node_modules`.** A fresh worktree has none. Check scripts that import only node builtins still run, but `sync-skill-versions.mjs` and vitest die with `MODULE_NOT_FOUND`: `ln -sfn /node_modules node_modules`. 5. **Apply, then check what is staged.** `git apply --check ` before `git apply `; `git add` only your paths; `git status --porcelain | grep -v '^M '` must come back empty, because anything left unstaged or untracked silently misses the PR. 6. **Clean the original workspace with `git restore ` only.** Not `git checkout -- .`, not `git stash` — either takes the other person's work with it. Their files must stay modified exactly as they were. 7. **Rule out a duplicate before starting.** `git worktree list` can show a `prunable` entry whose branch overlaps yours; `gh pr list --head --state all` says whether it already merged. An overlapping branch is more often the precedent to extend than parallel work. Run the repo's gates in the new worktree, not the original one — the new tree is the only one being pushed. ## Command mapping See `references/command-catalog.md`.