Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/commit-helper-passthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@naverpay/commit-helper": major
---

Add `passthrough` for Jira/Linear-style issue keys, and make re-tagging match only your branch's own tag.

**`passthrough`** — list your project keys, and branches that already contain the full key are tagged as-is. With `{ "passthrough": ["PROJ"] }`, branch `feature/PROJ-1871` becomes `[PROJ-1871]`. Only the keys you list are tagged, so unrelated text like `UTF-8` is never mistaken for an issue. Key detection matches [Jigit](https://marketplace.atlassian.com/apps/1217129), so a branch links the same way in Jira and here.

**Breaking change** — commit-helper skips a commit that is already tagged, and what counts as "already tagged" changed. Before, *any* `[#…]` tag in the message stopped it. Now, only *your current branch's own* tag does. For example, on branch `feature/123`, a message you wrote as `[#999] fix` used to be left alone, but now becomes `[#123] [#999] fix`. Re-running the hook or `git commit --amend` still never adds your tag twice — this now includes verbatim keys like `[PROJ-1871]`, which the old check could not detect.
36 changes: 34 additions & 2 deletions packages/commit-helper/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ git commit -m "새로운 기능 추가"
- `feature/123` → `[#123] 메시지`
- `qa/456` → `[your-org/your-repo#456] 메시지`
- `hotfix/789-urgent` → `[#789] 메시지`
- `feature/PROJ-1871` → `[PROJ-1871] 메시지` (passthrough 로 Jira/Linear 키 지원)

### 🛡️ 브랜치 보호

Expand Down Expand Up @@ -90,6 +91,27 @@ git commit -m "새로운 기능 추가"
- 키: 브랜치 접두사 (예: `"feature"`)
- 값: 저장소 이름 또는 현재 저장소인 경우 `null`

#### `passthrough` (배열)

이슈 키에 이미 프로젝트가 포함된 트래커(Jira/Linear 스타일 `PROJ-1871`)를 위한 프로젝트 키 목록. `rules` 가 브랜치 접두사를 저장소 참조로 변환하는 것과 달리, 등록된 키는 커밋 메시지에 **그대로** 복사됩니다.

```json
{ "passthrough": ["PROJ", "OPS"] }
```

`feature/PROJ-1871` 브랜치는 `[PROJ-1871]` 로 태깅됩니다. 등록된 프로젝트만 인식하므로 `UTF-8` 같은 무관한 `대문자-숫자` 토큰이 이슈로 오인되지 않습니다.

키 인식은 [Jigit](https://marketplace.atlassian.com/apps/1217129)(Jira↔Git 연동)과 동일합니다. 키는 브랜치 어디에나 올 수 있는 `<PROJECT>-<NUMBER>` 형태이며, `PROJECT` 는 대문자로 시작해 대문자/숫자가 이어지고(2자 이상), `NUMBER` 는 1~7자리 숫자입니다.

| 브랜치 (`["PROJ"]` 기준) | 결과 |
| ----------------------------- | ------------------------- |
| `feature/PROJ-1871` | `[PROJ-1871]` |
| `feature/PROJ-1871-add-login` | `[PROJ-1871]` |
| `feature/OPS-42` | 태깅 안 됨 (`OPS` 미등록) |
| `feature/PROJ-12345678` | 태깅 안 됨 (8자리 이상) |

브랜치가 둘 다에 매칭되면 `rules` 가 `passthrough` 보다 우선합니다. `feature` 와 `repo` 는 빌트인 prefix 규칙(`rules` 에 없어도 항상 활성)이라, `feature/311` 같은 `<prefix>/<number>` 브랜치는 passthrough 키가 아니라 prefix 경로로 `[#311]` 태깅됩니다. `feature/PROJ-311` 처럼 슬래시 뒤에 숫자가 아닌 글자가 오는 verbatim 키는 영향받지 않습니다.

#### `extends` (문자열)

설정을 상속받을 URL:
Expand All @@ -105,11 +127,21 @@ git commit -m "새로운 기능 추가"
### 기본 기능 브랜치

```bash
git checkout -b feature/NP-1234-결제-통합
git checkout -b feature/1234-결제-통합
git commit -m "결제 게이트웨이 구현"
# 결과: [#1234] 결제 게이트웨이 구현
```

### Jira/Linear 이슈 키 (passthrough)

`{ "passthrough": ["PROJ"] }` 설정 시:

```bash
git checkout -b feature/PROJ-1871-결제-통합
git commit -m "결제 게이트웨이 구현"
# 결과: [PROJ-1871] 결제 게이트웨이 구현
```

### 외부 저장소 참조

설정 파일:
Expand Down Expand Up @@ -183,7 +215,7 @@ npx @naverpay/commit-helper <commit-msg-file>
## 자주 묻는 질문

**Q: 이미 이슈 태그가 있는 경우에도 작동하나요?**
A: 네, 커밋 메시지에 이미 `[#123]` 같은 태그가 있으면 commit-helper는 건너뜁니다.
A: 메시지에 **현재 브랜치의** 참조(예: `feature/123` 의 `[#123]`)가 이미 있으면 그대로 둡니다 — 재실행/`git commit --amend` 에 안전합니다. 단, _다른_ 이슈 태그(예: 수동으로 넣은 `[#999]`)는 브랜치 참조 추가를 막지 않습니다.

**Q: 여러 이슈 번호를 사용할 수 있나요?**
A: 브랜치 이름에서는 하나의 이슈 번호만 지원하지만, 커밋 메시지에 수동으로 추가할 수 있습니다.
Expand Down
36 changes: 34 additions & 2 deletions packages/commit-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Extracts issue numbers from branch names and adds them to commit messages:
- `feature/123` → `[#123] your message`
- `qa/456` → `[your-org/your-repo#456] your message`
- `hotfix/789-urgent` → `[#789] your message`
- `feature/PROJ-1871` → `[PROJ-1871] your message` (Jira/Linear keys via `passthrough`)

### 🛡️ Branch Protection

Expand Down Expand Up @@ -90,6 +91,27 @@ Mapping of branch prefixes to repository names.
- Key: Branch prefix (e.g., `"feature"`)
- Value: Repository name or `null` for current repo

#### `passthrough` (array)

Project keys for trackers whose issue key already contains the project (Jira/Linear-style `PROJ-1871`). Listed keys are copied **verbatim** into the commit message — unlike `rules`, which translate a branch prefix into a repo reference.

```json
{ "passthrough": ["PROJ", "OPS"] }
```

A branch like `feature/PROJ-1871` is tagged `[PROJ-1871]`. Only listed projects are recognized, so unrelated `UPPERCASE-NUMBER` tokens (e.g. `UTF-8`) are never mistaken for issues.

Key recognition matches [Jigit](https://marketplace.atlassian.com/apps/1217129) (the Jira↔Git integration): a key is `<PROJECT>-<NUMBER>` found anywhere in the branch, where `PROJECT` is an uppercase letter followed by uppercase letters/digits (≥2 chars) and `NUMBER` is 1–7 digits.

| Branch (with `["PROJ"]`) | Result |
| ----------------------------- | ----------------------------- |
| `feature/PROJ-1871` | `[PROJ-1871]` |
| `feature/PROJ-1871-add-login` | `[PROJ-1871]` |
| `feature/OPS-42` | not tagged (`OPS` not listed) |
| `feature/PROJ-12345678` | not tagged (8+ digits) |

`rules` take precedence over `passthrough` when a branch matches both. Note that `feature` and `repo` are built-in prefix rules (active even without a `rules` entry), so a `<prefix>/<number>` branch like `feature/311` is tagged `[#311]` by the prefix path rather than as a passthrough key. Verbatim keys such as `feature/PROJ-311` are unaffected — a letter, not a digit, follows the slash.

#### `extends` (string)

URL to inherit configuration from:
Expand All @@ -105,11 +127,21 @@ URL to inherit configuration from:
### Basic Feature Branch

```bash
git checkout -b feature/NP-1234-payment-integration
git checkout -b feature/1234-payment-integration
git commit -m "Implement payment gateway"
# Result: [#1234] Implement payment gateway
```

### Jira/Linear Issue Key (passthrough)

With `{ "passthrough": ["PROJ"] }`:

```bash
git checkout -b feature/PROJ-1871-payment-integration
git commit -m "Implement payment gateway"
# Result: [PROJ-1871] Implement payment gateway
```

### External Repository Reference

With configuration:
Expand Down Expand Up @@ -183,7 +215,7 @@ npx @naverpay/commit-helper <commit-msg-file>
## FAQ

**Q: Does it work with existing issue tags?**
A: Yes, if your commit message already contains a tag like `[#123]`, commit-helper will skip it.
A: If the message already contains **your current branch's** reference (e.g. `[#123]` on `feature/123`), it is left unchanged — safe on re-run and `git commit --amend`. A tag for a _different_ issue (e.g. a hand-written `[#999]`) does not stop your branch's reference from being added.

**Q: Can I use multiple issue numbers?**
A: The branch name supports one issue number, but you can manually add more in your commit message.
Expand Down
120 changes: 97 additions & 23 deletions packages/commit-helper/__test__/cli.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
import {getCommitMessageByBranchName, isStringMatchingPatterns} from '../bin/cli.js'
import {ISSUE_TAGGING_REGEX, BRANCH_ISSUE_TAGGING_REGEX} from '../src/constant.js'

describe('커밋 메시지내 이슈를 찾는 정규식 테스트', () => {
it.each([
['일반적인 메시지', false],
['naverpay/cli#1', false],
['naverpay/cli#1]', false],
['naverpay/cli#1] 결제는 네이버페이로', false],
['[naverpay/cli#1', false],
['[naverpay/cli#1 결제는 네이버페이로', false],
['[naverpay/cli#1]', true],
['[naverpay/cli#1] 결제는 네이버페이로', true],
['[naverpay/cli#12345] 결제는 네이버페이로', true],
['[naverpay/cli#123456] 결제는 네이버페이로', false],
['[#12345] 결제는 네이버페이로', true],
['[#123] 결제는 네이버페이로', true],
['[#123456] 결제는 네이버페이로', false],
])('커밋 메시지 "%s"는 내부에 이슈 태깅이 있다 => (%s)', (message, result) => {
const output = message.match(ISSUE_TAGGING_REGEX) !== null
expect(output).toBe(result)
})
})
import {getCommitMessageByBranchName, isStringMatchingPatterns, resolveKey, alreadyHasRef} from '../bin/cli.js'
import {BRANCH_ISSUE_TAGGING_REGEX} from '../src/constant.js'

describe('브랜치가 commithelper 형식에 맞는지 확인하는 정규식', () => {
it.each([
Expand Down Expand Up @@ -84,3 +63,98 @@ describe('', () => {
expect(output).toBe(result)
})
})

/**
* @description passthrough 키 인식 (commithelper-go 의 resolveKey 와 동일 규칙).
*/
describe('resolveKey: passthrough 키 인식', () => {
it.each([
[['PROJ'], 'feature/PROJ-1871', 'PROJ-1871'], // 등록된 키
[['PROJ'], 'PROJ-1871', 'PROJ-1871'], // prefix 없는 순수 키
[['PROJ'], 'feature/PROJ-1871-add-login', 'PROJ-1871'], // 설명 suffix 무시
[['PROJ'], 'feature/PROJ-1871-20260101', 'PROJ-1871'], // 날짜 suffix 인식 (Jigit 규칙)
[['PROJ'], 'feature/PROJ-1871_wip', 'PROJ-1871'], // underscore 인식 (Jigit 규칙)
[['PROJ'], 'feature/OPS-42', null], // 미등록 프로젝트
[['PROJ'], 'feature/PROJ-12345678', null], // 8자리 숫자 거부
[['PROJ'], 'XPROJ-123', null], // 더 긴 키 안에서는 프로젝트가 매칭되지 않음
[['PROJ'], 'chore/UTF-8-PROJ-123', 'PROJ-123'], // 잡토큰 건너뛰고 등록된 키 선택
[['proj'], 'feature/PROJ-1', null], // 소문자 목록은 매칭되지 않음
[undefined, 'feature/PROJ-1871', null], // passthrough 없으면 비활성
[[], 'feature/PROJ-1871', null], // 빈 목록도 비활성
])('resolveKey("%s", %o) === %s', (passthrough, branch, expected) => {
expect(resolveKey(branch, passthrough)).toBe(expected)
})
})

/**
* @description Jigit 문서에 나오는 키 인식 케이스와 동일하게 동작하는지 (["ABC"] 기준).
*/
describe('resolveKey: Jigit 문서 케이스 패리티', () => {
it.each([
['ABC-123', 'ABC-123'],
['feature/ABC-123', 'ABC-123'],
['feature_ABC-123', 'ABC-123'],
['feature/ABC-123-modal', 'ABC-123'],
['ABC-123_modal', 'ABC-123'],
['[ABC-123] feature', 'ABC-123'],
['release/test/ABC-123/fix', 'ABC-123'],
['ABC-123-4', 'ABC-123'],
['ABC-12345678', null],
['abc-123', null],
['Abc-123', null],
['ABC_123', null],
['ABC123', null],
['ABC-abc', null],
])('resolveKey("%s", ["ABC"]) === %s', (branch, expected) => {
expect(resolveKey(branch, ['ABC'])).toBe(expected)
})
})

/**
* @description 멱등 태깅 경계 (commithelper-go 의 alreadyHasRef 와 동일).
*/
describe('alreadyHasRef: 멱등 태깅 경계', () => {
it.each([
['[#123] fix', '#123', true], // 앞쪽 기본 태그
['[#1234] fix', '#123', false], // 더 긴 숫자는 매칭 아님
['fix\n\nRef. [#123]', '#123', true], // 본문 하단 참조 (template)
['[PROJ-1871] fix', 'PROJ-1871', true], // verbatim 키 존재
['[org/repo#123] fix', 'org/repo#123', true], // cross-repo 참조 존재
['fix login', '#123', false], // 없음
['see AB-1abc here', 'AB-1', false], // 참조 뒤에 글자가 오면 매칭 아님
['[MY-PROJ-1871] earlier', 'PROJ-1871', false], // 더 긴 하이픈 키 안에서는 매칭 아님
])('alreadyHasRef("%s", "%s") === %s', (message, ref, expected) => {
expect(alreadyHasRef(message, ref)).toBe(expected)
})
})

/**
* @description resolve 우선순위(prefix > passthrough)와 멱등성 end-to-end.
*/
describe('getCommitMessageByBranchName: 우선순위와 멱등', () => {
const rules = {feature: null}
const passthrough = ['ABC', 'PROJ']

it.each([
// prefix 해석 (JS 정규식 기준)
['my-team/11', '메시지', {'my-team': 'my-org/my-repo'}, undefined, '[my-org/my-repo#11] 메시지'],
['feature/42', '메시지', {feature: null}, undefined, '[#42] 메시지'],
['main', '메시지', {feature: null}, undefined, '메시지'],
['unknown/99', '메시지', {feature: null}, undefined, '메시지'],
['feature/PROJ-1', '메시지', {feature: null}, undefined, '메시지'], // prefix 모양 아님 + passthrough 없음
// 우선순위
['feature/123', '메시지', rules, passthrough, '[#123] 메시지'], // github prefix
['feature/ABC-99', '메시지', rules, passthrough, '[ABC-99] 메시지'], // verbatim 키
['feature/12-ABC-34', '메시지', rules, passthrough, '[#12] 메시지'], // 둘 다 매칭 → prefix 우선
['wip', '메시지', rules, passthrough, '메시지'], // 둘 다 아님
// 멱등 / verbatim
['feature/PROJ-1871', 'fix', {}, ['PROJ'], '[PROJ-1871] fix'], // verbatim 태깅
['feature/123', '[#123] fix', {feature: null}, undefined, '[#123] fix'], // 재실행 멱등
['feature/PROJ-1871', '[MY-PROJ-1871] earlier', {}, ['PROJ'], '[PROJ-1871] [MY-PROJ-1871] earlier'], // 임베디드 키는 태깅됨으로 보지 않음
['feature/PROJ-1', 'work on PROJ-1abc', {}, ['PROJ'], '[PROJ-1] work on PROJ-1abc'], // 뒤에 글자 오는 건 태깅됨 아님
// behavior-change lock (major 사유): 다른 이슈 태그는 브랜치 참조 추가를 막지 않는다
['feature/123', '[#999] fix', {feature: null}, undefined, '[#123] [#999] fix'],
])('[%s] "%s" → "%s"', (branch, message, rulesMap, pass, expected) => {
expect(getCommitMessageByBranchName(branch, message, rulesMap, pass)).toBe(expected)
})
})
Loading
Loading