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
7 changes: 7 additions & 0 deletions .changeset/great-pumas-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@naverpay/commit-helper": patch
---

feat(commit-helper): support local file path in extends

PR: [feat(commit-helper): support local file path in extends](https://github.com/NaverPayDev/cli/pull/84)
25 changes: 24 additions & 1 deletion packages/commit-helper/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,37 @@ git commit -m "새로운 기능 추가"

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

설정을 상속받을 URL:
공유 베이스 설정을 상속받을 **HTTP/HTTPS URL** 또는 **로컬 파일 경로**:

**원격 URL:**

```json
{
"extends": "https://raw.githubusercontent.com/naverpay/standards/main/.commithelperrc.json"
}
```

**로컬 파일 경로** (사내 레지스트리 등 인증이 필요한 환경에서 권장):

```json
{
"extends": "./node_modules/@my-org/commithelperrc/.commithelperrc.json"
}
```

공유 설정을 npm 패키지로 배포하고 devDependency로 설치한 뒤 로컬 경로로 참조합니다. 오프라인에서도 동작하고, `package.json`으로 버전 관리되며, 별도 인증이 필요 없습니다.

각 레포는 고유 설정만 선언하면 됩니다:

```json
{
"extends": "./node_modules/@my-org/commithelperrc/.commithelperrc.json",
"rules": {
"my-feature": "my-org/my-repo"
}
}
```

## 예제

### 기본 기능 브랜치
Expand Down
17 changes: 16 additions & 1 deletion packages/commit-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,29 @@ Key recognition matches [Jigit](https://marketplace.atlassian.com/apps/1217129)

#### `extends` (string)

URL to inherit configuration from:
Base config to inherit from. Accepts an **HTTP/HTTPS URL** or a **local file path**:

**Remote URL:**

```json
{
"extends": "https://raw.githubusercontent.com/naverpay/standards/main/.commithelperrc.json"
}
```

**Local file path** (recommended for private registries — install shared config as a dev dependency):

```json
{
"extends": "./node_modules/@my-org/commithelperrc/.commithelperrc.json",
"rules": {
"my-feature": "my-org/my-repo"
}
}
```

Works offline, versioned through `package.json`, no auth required.

## Examples

### Basic Feature Branch
Expand Down
44 changes: 44 additions & 0 deletions packages/commit-helper/__test__/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,47 @@ describe('getCommitMessageByBranchName: 우선순위와 멱등', () => {
expect(getCommitMessageByBranchName(branch, message, rulesMap, pass)).toBe(expected)
})
})

// ── extends: loadExtendsConfig ────────────────────────────────────────────────

import {loadExtendsConfig} from '../bin/cli.js'
import {writeFile, mkdtemp, rm} from 'fs/promises'
import {tmpdir} from 'os'
import {join} from 'path'

describe('loadExtendsConfig — local file path', () => {
let dir

beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'commithelper-test-'))
})

afterEach(async () => {
await rm(dir, {recursive: true, force: true})
})

it('로컬 파일의 rules, protect, passthrough를 파싱한다', async () => {
const config = {
rules: {plan: 'org/plan', qa: 'org/qa'},
protect: ['main', 'master'],
passthrough: ['PROJ'],
}
const filePath = join(dir, '.commithelperrc.json')
await writeFile(filePath, JSON.stringify(config))

const result = await loadExtendsConfig(filePath)
expect(result.rules).toEqual(config.rules)
expect(result.protect).toEqual(config.protect)
expect(result.passthrough).toEqual(config.passthrough)
})

it('파일이 없으면 에러를 던진다', async () => {
await expect(loadExtendsConfig(join(dir, 'not-exist.json'))).rejects.toThrow()
})

it('유효하지 않은 JSON이면 에러를 던진다', async () => {
const filePath = join(dir, '.commithelperrc.json')
await writeFile(filePath, 'not json')
await expect(loadExtendsConfig(filePath)).rejects.toThrow()
})
})
28 changes: 19 additions & 9 deletions packages/commit-helper/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,21 @@ interface Config {
passthrough?: string[]
}

// loadExtendsConfig dispatches to HTTP fetch or local file read based on the extends value.
export async function loadExtendsConfig(extendsValue: string): Promise<Partial<Config>> {
if (/^https?:\/\//.test(extendsValue)) {
const response = await fetch(extendsValue)
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`)
}
return response.json() as Promise<Partial<Config>>
}
// local file path — relative paths resolved from cwd (repo root)
const filePath = isAbsolute(extendsValue) ? extendsValue : join(process.cwd(), extendsValue)
const content = await fs.readFile(filePath, 'utf8')
return JSON.parse(content) as Partial<Config>
}

export async function readExternalConfig(): Promise<Config> {
const explorerSync = cosmiconfigSync('commithelper')
const searchedFor = explorerSync.search()
Expand All @@ -160,15 +175,10 @@ export async function readExternalConfig(): Promise<Config> {
let mergedProtect: string[] = Array.isArray(localConfig.protect) ? [...localConfig.protect] : []
let mergedPassthrough: string[] = Array.isArray(localConfig.passthrough) ? [...localConfig.passthrough] : []

const extendsUrl = localConfig.extends
if (typeof extendsUrl === 'string' && /^(http|https):\/\//.test(extendsUrl)) {
const extendsValue = localConfig.extends
if (typeof extendsValue === 'string' && extendsValue.trim() !== '') {
try {
const response = await fetch(extendsUrl)
if (!response.ok) {
throw new Error(`Failed to fetch extends config: ${response.status} ${response.statusText}`)
}

const extendsConfig = (await response.json()) as Partial<Config>
const extendsConfig = await loadExtendsConfig(extendsValue)

if (extendsConfig.rules && typeof extendsConfig.rules === 'object') {
Object.assign(mergedRules, extendsConfig.rules)
Expand All @@ -182,7 +192,7 @@ export async function readExternalConfig(): Promise<Config> {
mergedPassthrough = [...extendsConfig.passthrough, ...mergedPassthrough]
}
} catch (e) {
throw new Error(`Failed to load external config from "${extendsUrl}": ${(e as Error).message}`)
throw new Error(`Failed to load extends config from "${extendsValue}": ${(e as Error).message}`)
}
}

Expand Down
3 changes: 1 addition & 2 deletions packages/commit-helper/src/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"properties": {
"extends": {
"type": "string",
"pattern": "^(http|https)://",
"description": "Extends remote URL"
"description": "Base config to inherit from. Accepts an http:// or https:// URL, or a local file path (e.g. ./node_modules/@my-org/commithelperrc/.commithelperrc.json)."
},
"protect": {
"type": "array",
Expand Down
Loading