Skip to content

Commit 6f55332

Browse files
killagu-clawclaudekillagugemini-code-assist[bot]
authored
chore: add git-branch-porter skill for Claude Code (#407)
## Summary - Add a project-level Claude Code skill (`git-branch-porter`) that documents the workflow for porting commits between divergent long-lived branches (e.g., master → next) - Includes reference docs for CJS→ESM conversion patterns and pnpm catalog/dedupe troubleshooting - Skill is auto-loaded by Claude Code from `.claude/skills/` when working in this repo ## Files ``` .claude/skills/git-branch-porter/ ├── SKILL.md # 7-step porting workflow └── references/ ├── esm-patterns.md # CJS → ESM conversion catalog └── pnpm-troubleshooting.md # pnpm workspace fixes ``` ## Test plan - [x] Skill validates and packages successfully via skill-creator - [x] Claude Code loads the skill from project `.claude/skills/` directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added Git Branch Porter workflow guide with step-by-step instructions for cherry-picking commits across branches * Added ESM conversion patterns reference with code examples * Added pnpm workspace troubleshooting guide covering dependency management and configuration best practices <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: killa <killa07071201@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 1e75cae commit 6f55332

File tree

3 files changed

+366
-0
lines changed

3 files changed

+366
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
name: git-branch-porter
3+
description: Port (cherry-pick) commits between long-lived divergent branches in monorepos, especially CJS-to-ESM migrations. Use when asked to backport, forward-port, or cherry-pick batches of commits across branches that have diverged significantly (e.g., master → next). Covers git worktree setup, batch cherry-picking, ESM conversion, pnpm workspace/catalog fixes, test troubleshooting, and CI verification.
4+
---
5+
6+
# Git Branch Porter
7+
8+
Port commits between divergent long-lived branches in a monorepo, handling module system differences (CJS → ESM), dependency management, and CI.
9+
10+
## Workflow
11+
12+
### 1. Setup
13+
14+
Use a **git worktree** to work on the target branch without disturbing the main checkout:
15+
16+
```bash
17+
git worktree add ../repo-port target-branch
18+
cd ../repo-port
19+
git checkout -b port/source-to-target
20+
git remote add fork git@github.com:USER/REPO.git # if using a fork
21+
```
22+
23+
### 2. Identify Commits
24+
25+
Find commits on the source branch not yet on the target:
26+
27+
```bash
28+
# From target branch, find divergence point
29+
git log --oneline target-branch..source-branch
30+
```
31+
32+
Review each commit to assess portability. Skip version bumps (`chore(release)`) and commits already applied.
33+
34+
### 3. Batch Cherry-Pick
35+
36+
Cherry-pick in small batches (5-10 commits) for manageable conflict resolution:
37+
38+
```bash
39+
git cherry-pick <oldest>^..<newest> --no-commit
40+
# Resolve conflicts, then:
41+
git commit --no-verify -m "port: batch N - description"
42+
```
43+
44+
Use `--no-commit` to stage all changes, fix ESM issues, then commit once. When conflicts arise, prefer the target branch's patterns (ESM) over the source's (CJS).
45+
46+
### 4. Apply ESM Fixes
47+
48+
After cherry-picking CJS code onto an ESM branch, apply conversions. See [references/esm-patterns.md](references/esm-patterns.md) for the complete pattern catalog.
49+
50+
Key conversions:
51+
- `require()``import` / `import type`
52+
- `module.exports``export default`
53+
- Add `.ts` extensions to relative imports
54+
- `__dirname``import.meta.dirname`
55+
- Router/config files: convert to `export default` function
56+
57+
### 5. Fix Dependencies
58+
59+
pnpm workspaces with `catalog:` protocol need special attention when versions drift. See [references/pnpm-troubleshooting.md](references/pnpm-troubleshooting.md) for common issues.
60+
61+
Key patterns:
62+
- Pin floating dist-tags to exact versions in `pnpm-workspace.yaml` catalog
63+
- Use pnpm `overrides` in root `package.json` for transitive dependency control
64+
- Run `pnpm dedupe` after changes, verify with `pnpm dedupe --check`
65+
- Add package `exports` entries when downstream packages need new entry points
66+
67+
### 6. Test & Verify
68+
69+
Run the full test suite locally before pushing:
70+
71+
```bash
72+
pnpm test # vitest (core packages)
73+
pnpm run test:mocha # mocha (plugin packages)
74+
pnpm dedupe --check # lockfile consistency
75+
```
76+
77+
Common test fixes:
78+
- Increase timeouts for app boot under concurrent load: `beforeAll(fn, 30_000)`
79+
- Fix import paths in test fixtures (CJS → ESM)
80+
- Update mock configurations for ESM module loading
81+
82+
### 7. Push & CI
83+
84+
```bash
85+
git push fork port/source-to-target
86+
gh pr create --base target-branch --title "port: commits from source"
87+
gh pr checks <PR_NUMBER> # monitor CI
88+
gh run view <RUN_ID> --log-failed # inspect failures
89+
```
90+
91+
## Tips
92+
93+
- Use `--no-verify` when pre-commit hooks fail due to cross-branch incompatibilities; run lint separately before the final push
94+
- Commit after each batch for easier bisection if issues arise
95+
- When `pnpm dedupe --check` fails in CI but passes locally, check for `--frozen-lockfile` differences — CI uses frozen lockfile which preserves exact lockfile state
96+
- For packages that need new export paths, add them to both `exports` in `package.json` and create the corresponding source file
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# ESM Conversion Patterns
2+
3+
Pattern catalog for converting CJS code to ESM when cherry-picking between branches.
4+
5+
## Import Conversions
6+
7+
### require → import
8+
9+
```javascript
10+
// CJS
11+
const { Foo, Bar } = require('./foo');
12+
const baz = require('baz');
13+
14+
// ESM
15+
import { Foo, Bar } from './foo.ts';
16+
import baz from 'baz';
17+
```
18+
19+
### Type-only imports
20+
21+
```typescript
22+
// CJS
23+
const { SomeType } = require('./types');
24+
25+
// ESM — use `import type` when the import is only used in type positions
26+
import type { SomeType } from './types.ts';
27+
```
28+
29+
### Dynamic require → dynamic import
30+
31+
```javascript
32+
// CJS
33+
const mod = require(dynamicPath);
34+
35+
// ESM
36+
const mod = await import(dynamicPath);
37+
```
38+
39+
## Export Conversions
40+
41+
### module.exports → export default
42+
43+
```javascript
44+
// CJS
45+
module.exports = function(app) { ... };
46+
47+
// ESM
48+
export default function(app) { ... };
49+
```
50+
51+
### Named exports
52+
53+
```javascript
54+
// CJS
55+
exports.foo = foo;
56+
exports.bar = bar;
57+
58+
// ESM
59+
export { foo, bar };
60+
```
61+
62+
## Path & Globals
63+
64+
### __dirname / __filename
65+
66+
```javascript
67+
// CJS
68+
const dir = __dirname;
69+
const file = __filename;
70+
71+
// ESM (Node 20.11+ / 21.2+)
72+
const dir = import.meta.dirname;
73+
const file = import.meta.filename;
74+
75+
// ESM (Node < 20.11)
76+
import { fileURLToPath } from 'node:url';
77+
import { dirname } from 'node:path';
78+
const file = fileURLToPath(import.meta.url);
79+
const dir = dirname(file);
80+
```
81+
82+
### Relative import extensions
83+
84+
In ESM source (TypeScript with `"type": "module"`), relative imports must include the file extension:
85+
86+
```typescript
87+
// Wrong
88+
import { Foo } from './foo';
89+
90+
// Correct — use .ts in source when TypeScript resolves .ts files directly
91+
import { Foo } from './foo.ts';
92+
93+
// Correct — use .js when TypeScript emits .js files (outDir build)
94+
import { Foo } from './foo.js';
95+
```
96+
97+
Check the project's `tsconfig.json` `moduleResolution` and build setup to determine which extension to use.
98+
99+
## Egg.js-Specific Patterns
100+
101+
### Router files
102+
103+
```typescript
104+
// CJS
105+
module.exports = app => {
106+
app.router.get('/', app.controller.home.index);
107+
};
108+
109+
// ESM
110+
import type { Application } from 'egg';
111+
export default (app: Application) => {
112+
app.router.get('/', app.controller.home.index);
113+
};
114+
```
115+
116+
### Config files
117+
118+
```typescript
119+
// CJS
120+
module.exports = appInfo => {
121+
const config = {};
122+
config.keys = appInfo.name;
123+
return config;
124+
};
125+
126+
// ESM
127+
import type { EggAppConfig } from 'egg';
128+
export default (appInfo: { name: string }) => {
129+
const config = {} as Partial<EggAppConfig>;
130+
config.keys = appInfo.name;
131+
return config;
132+
};
133+
```
134+
135+
### Plugin config
136+
137+
```typescript
138+
// CJS
139+
exports.tegg = { enable: true, package: '@eggjs/tegg-plugin' };
140+
141+
// ESM
142+
export const tegg = { enable: true, package: '@eggjs/tegg-plugin' };
143+
```
144+
145+
## Test Fixture Conversions
146+
147+
Test fixtures often have their own `package.json`. Ensure:
148+
- `"type": "module"` is set
149+
- All imports use ESM syntax
150+
- Mock setup uses ESM-compatible patterns
151+
152+
```typescript
153+
// CJS test
154+
const mm = require('egg-mock');
155+
const assert = require('assert');
156+
157+
// ESM test
158+
import { strict as assert } from 'node:assert';
159+
import mm from '@eggjs/mock';
160+
```
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# pnpm Workspace Troubleshooting
2+
3+
Common issues when porting commits in pnpm monorepos with catalog mode.
4+
5+
## Floating Dist-Tags Resolve to Incompatible Versions
6+
7+
**Problem**: Catalog uses floating dist-tags like `egg: beta` which resolve to the latest beta. Over time, the resolved version changes and may be incompatible.
8+
9+
**Symptom**: `pnpm install --force` or `pnpm dedupe` upgrades transitive dependencies, breaking tests with errors like `GlobalGraph.instance is not set` or bundled plugin conflicts.
10+
11+
**Fix**: Pin exact versions in both catalog and overrides:
12+
13+
```yaml
14+
# pnpm-workspace.yaml
15+
catalog:
16+
egg: 4.1.0-beta.26 # was: beta
17+
'@eggjs/mock': 7.0.0-beta.26
18+
'@eggjs/bin': 8.0.0-beta.26
19+
```
20+
21+
```json
22+
// package.json
23+
{
24+
"pnpm": {
25+
"overrides": {
26+
"egg": "4.1.0-beta.26",
27+
"@eggjs/mock": "7.0.0-beta.26",
28+
"@eggjs/bin": "8.0.0-beta.26"
29+
}
30+
}
31+
}
32+
```
33+
34+
**Why both?** Catalog pins affect workspace packages. Overrides affect transitive dependencies from published packages (e.g., `standalone``egg@beta``@eggjs/ajv-plugin@beta.36`).
35+
36+
**Note on catalog and publishing**: `catalog:` protocol is resolved to actual versions at publish time. Pinning in catalog affects what gets published. Overrides are workspace-only and don't affect published packages.
37+
38+
## pnpm dedupe --check Fails in CI but Passes Locally
39+
40+
**Problem**: CI uses `pnpm install --frozen-lockfile` which preserves the exact lockfile, while local `pnpm install` may subtly adjust resolution.
41+
42+
**Common causes**:
43+
1. Peer dependency resolution differences (e.g., `axios@1.13.5` vs `axios@1.13.5(debug@4.4.3)`)
44+
2. Different pnpm store state between local and CI
45+
46+
**Fix approaches**:
47+
1. Run `pnpm dedupe` locally, commit the updated lockfile
48+
2. If dedupe creates a circular issue (fix → check → wants reverse), try:
49+
- Add overrides for the problematic package
50+
- Delete `node_modules` and `pnpm-lock.yaml`, run fresh `pnpm install`, then `pnpm dedupe`
51+
- Pin the problematic transitive dependency version
52+
53+
## Adding New Package Export Paths
54+
55+
**Problem**: A published package at version X doesn't export a path that downstream packages need (e.g., `@eggjs/ajv-plugin@beta.36` imports `@eggjs/tegg-plugin/types` but the workspace version doesn't export it).
56+
57+
**Fix**: Add the export in the workspace package:
58+
59+
```json
60+
// plugin/tegg/package.json
61+
{
62+
"exports": {
63+
".": "./src/index.ts",
64+
"./types": "./src/types.ts", // add new export
65+
"./package.json": "./package.json"
66+
}
67+
}
68+
```
69+
70+
Create the source file if it doesn't exist. For example, `plugin/ajv` needs to re-export types from `@eggjs/tegg-plugin`:
71+
72+
```typescript
73+
// plugin/ajv/src/types.ts
74+
export {}; // This creates an empty module. If re-exporting, change to 'export * from ...'
75+
```
76+
77+
## Bundled Dependencies in Newer Versions
78+
79+
**Problem**: A newer version of a package (e.g., `egg@4.1.0-beta.36`) bundles sub-packages (like `@eggjs/aop-plugin`, `@eggjs/eventbus-plugin`) that conflict with workspace versions.
80+
81+
**Symptom**: Published bundled packages have `GlobalGraph.instance` assertions that fail because they get a different `GlobalGraph` singleton from the workspace packages.
82+
83+
**Fix**: Pin to the older version that doesn't bundle these packages, using both catalog pins and overrides as described above.
84+
85+
## Test Timeout Issues Under Concurrent Load
86+
87+
**Problem**: vitest runs tests concurrently. App boot in `beforeAll` hooks can take >5s under load, exceeding the default 5000ms timeout.
88+
89+
**Fix**: Add explicit timeouts to slow hooks:
90+
91+
```typescript
92+
// vitest
93+
beforeAll(async () => {
94+
app = mm.app({ baseDir: '...' });
95+
await app.ready();
96+
}, 30_000); // 30 second timeout
97+
98+
it('slow test', async () => {
99+
// ...
100+
}, 30_000);
101+
```
102+
103+
For mocha, set timeout in the describe/it block:
104+
105+
```typescript
106+
describe('suite', function() {
107+
this.timeout(30000);
108+
// ...
109+
});
110+
```

0 commit comments

Comments
 (0)