From 46aae9750f7a316f048e478cc1356893c2f6f7fb Mon Sep 17 00:00:00 2001 From: kyungmi-k Date: Fri, 3 Jul 2026 13:37:17 +0900 Subject: [PATCH 1/3] feat(commit-helper): support local file path in extends --- packages/commit-helper/README.ko.md | 25 +++++++++++- packages/commit-helper/__test__/cli.test.js | 44 +++++++++++++++++++++ packages/commit-helper/bin/cli.ts | 28 ++++++++----- packages/commit-helper/src/schema.json | 3 +- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/packages/commit-helper/README.ko.md b/packages/commit-helper/README.ko.md index d738a5f..67d910b 100644 --- a/packages/commit-helper/README.ko.md +++ b/packages/commit-helper/README.ko.md @@ -114,7 +114,9 @@ git commit -m "새로운 기능 추가" #### `extends` (문자열) -설정을 상속받을 URL: +공유 베이스 설정을 상속받을 **HTTP/HTTPS URL** 또는 **로컬 파일 경로**: + +**원격 URL:** ```json { @@ -122,6 +124,27 @@ git commit -m "새로운 기능 추가" } ``` +**로컬 파일 경로** (사내 레지스트리 등 인증이 필요한 환경에서 권장): + +```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" + } +} +``` + ## 예제 ### 기본 기능 브랜치 diff --git a/packages/commit-helper/__test__/cli.test.js b/packages/commit-helper/__test__/cli.test.js index d339867..f1e345d 100644 --- a/packages/commit-helper/__test__/cli.test.js +++ b/packages/commit-helper/__test__/cli.test.js @@ -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() + }) +}) diff --git a/packages/commit-helper/bin/cli.ts b/packages/commit-helper/bin/cli.ts index de2b57e..b7320bd 100644 --- a/packages/commit-helper/bin/cli.ts +++ b/packages/commit-helper/bin/cli.ts @@ -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> { + 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> + } + // 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 +} + export async function readExternalConfig(): Promise { const explorerSync = cosmiconfigSync('commithelper') const searchedFor = explorerSync.search() @@ -160,15 +175,10 @@ export async function readExternalConfig(): Promise { 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 + const extendsConfig = await loadExtendsConfig(extendsValue) if (extendsConfig.rules && typeof extendsConfig.rules === 'object') { Object.assign(mergedRules, extendsConfig.rules) @@ -182,7 +192,7 @@ export async function readExternalConfig(): Promise { 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}`) } } diff --git a/packages/commit-helper/src/schema.json b/packages/commit-helper/src/schema.json index f88a394..c9a1191 100644 --- a/packages/commit-helper/src/schema.json +++ b/packages/commit-helper/src/schema.json @@ -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", From 356520e742fb12ba8f516a3b78d6c6760cb67b52 Mon Sep 17 00:00:00 2001 From: kyungmi-k Date: Fri, 3 Jul 2026 13:39:57 +0900 Subject: [PATCH 2/3] docs(commit-helper): add local file path example to README.md --- packages/commit-helper/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/commit-helper/README.md b/packages/commit-helper/README.md index a637b33..c563935 100644 --- a/packages/commit-helper/README.md +++ b/packages/commit-helper/README.md @@ -114,7 +114,9 @@ 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 { @@ -122,6 +124,19 @@ URL to inherit configuration from: } ``` +**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 From e937702adaec7bc2106a011636e5b9d468dccbee Mon Sep 17 00:00:00 2001 From: Koong Kyungmi Date: Fri, 3 Jul 2026 13:40:12 +0900 Subject: [PATCH 3/3] Create great-pumas-attack.md --- .changeset/great-pumas-attack.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/great-pumas-attack.md diff --git a/.changeset/great-pumas-attack.md b/.changeset/great-pumas-attack.md new file mode 100644 index 0000000..979fc2f --- /dev/null +++ b/.changeset/great-pumas-attack.md @@ -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)